Реализация аккордеона и табов на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация аккордеона и табов на сайте
Простая
~1 рабочий день
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация аккордеона и табов на сайте

Аккордеоны и табы — базовые паттерны компоновки контента. Сделать их «просто работающими» несложно. Сделать доступными, SEO-friendly и без мигания при загрузке — уже требует аккуратности.

Аккордеон: нативный HTML

Нативный <details>/<summary> работает без JS, имеет встроенную семантику и анимируется через CSS:

<div class="accordion" role="list">
  <details class="accordion__item" role="listitem">
    <summary class="accordion__trigger">
      Как оформить заказ?
      <span class="accordion__icon" aria-hidden="true"></span>
    </summary>
    <div class="accordion__content">
      <p>Выберите товары, перейдите в корзину и нажмите «Оформить заказ».</p>
    </div>
  </details>

  <details class="accordion__item" role="listitem">
    <summary class="accordion__trigger">
      Какие способы оплаты доступны?
    </summary>
    <div class="accordion__content">
      <p>Карта, наличные при получении, переводом.</p>
    </div>
  </details>
</div>
.accordion__item {
  border-bottom: 1px solid #e2e8f0;
}

.accordion__trigger {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 0;
  cursor: pointer;
  list-style: none;  /* убираем нативный маркер */
  font-weight: 500;
  user-select: none;
}

.accordion__trigger::-webkit-details-marker {
  display: none;  /* Safari */
}

.accordion__icon::before {
  content: '+';
  font-size: 20px;
  transition: transform 0.2s;
}

details[open] .accordion__icon::before {
  content: '−';
}

/* Анимация через @starting-style (Chrome 117+, Firefox 129+) */
.accordion__content {
  overflow: hidden;
}

@supports (interpolate-size: allow-keywords) {
  .accordion__content {
    interpolate-size: allow-keywords;
    height: 0;
    transition: height 0.3s ease;
  }

  details[open] .accordion__content {
    height: auto;
  }
}

Для браузеров без interpolate-size — JavaScript-анимация:

document.querySelectorAll('details.accordion__item').forEach(details => {
  const content = details.querySelector('.accordion__content') as HTMLElement

  details.addEventListener('toggle', () => {
    if (details.open) {
      const height = content.scrollHeight
      content.style.height = '0'
      requestAnimationFrame(() => {
        content.style.transition = 'height 0.3s ease'
        content.style.height = `${height}px`
        content.addEventListener('transitionend', () => {
          content.style.height = 'auto'
        }, { once: true })
      })
    } else {
      content.style.height = `${content.scrollHeight}px`
      requestAnimationFrame(() => {
        content.style.transition = 'height 0.3s ease'
        content.style.height = '0'
      })
    }
  })
})

Аккордеон: React-компонент

import { useState, useRef, useEffect } from 'react'

interface AccordionItem {
  id: string
  question: string
  answer: React.ReactNode
}

function AccordionItem({ item, isOpen, onToggle }: {
  item: AccordionItem
  isOpen: boolean
  onToggle: () => void
}) {
  const contentRef = useRef<HTMLDivElement>(null)
  const [height, setHeight] = useState(0)

  useEffect(() => {
    if (contentRef.current) {
      setHeight(isOpen ? contentRef.current.scrollHeight : 0)
    }
  }, [isOpen])

  return (
    <div className="accordion-item">
      <button
        className="accordion-item__trigger"
        onClick={onToggle}
        aria-expanded={isOpen}
        aria-controls={`accordion-content-${item.id}`}
        id={`accordion-btn-${item.id}`}
      >
        {item.question}
        <svg className={`accordion-item__chevron ${isOpen ? 'rotated' : ''}`} viewBox="0 0 24 24">
          <path d="M6 9l6 6 6-6" stroke="currentColor" fill="none" strokeWidth="2"/>
        </svg>
      </button>
      <div
        id={`accordion-content-${item.id}`}
        role="region"
        aria-labelledby={`accordion-btn-${item.id}`}
        style={{ height, overflow: 'hidden', transition: 'height 0.3s ease' }}
      >
        <div ref={contentRef} className="accordion-item__body">
          {item.answer}
        </div>
      </div>
    </div>
  )
}

export function Accordion({ items, allowMultiple = false }: {
  items: AccordionItem[]
  allowMultiple?: boolean
}) {
  const [openIds, setOpenIds] = useState<Set<string>>(new Set())

  function toggle(id: string) {
    setOpenIds(prev => {
      const next = new Set(prev)
      if (next.has(id)) {
        next.delete(id)
      } else {
        if (!allowMultiple) next.clear()
        next.add(id)
      }
      return next
    })
  }

  return (
    <div className="accordion">
      {items.map(item => (
        <AccordionItem
          key={item.id}
          item={item}
          isOpen={openIds.has(item.id)}
          onToggle={() => toggle(item.id)}
        />
      ))}
    </div>
  )
}

Табы: ARIA-паттерн

interface Tab {
  id: string
  label: string
  content: React.ReactNode
}

export function Tabs({ tabs, defaultTab }: { tabs: Tab[]; defaultTab?: string }) {
  const [activeId, setActiveId] = useState(defaultTab ?? tabs[0]?.id)
  const tablistRef = useRef<HTMLDivElement>(null)

  // Keyboard navigation по стрелкам
  function handleKeyDown(e: React.KeyboardEvent, currentIndex: number) {
    let newIndex = currentIndex
    if (e.key === 'ArrowRight') newIndex = (currentIndex + 1) % tabs.length
    else if (e.key === 'ArrowLeft') newIndex = (currentIndex - 1 + tabs.length) % tabs.length
    else if (e.key === 'Home') newIndex = 0
    else if (e.key === 'End') newIndex = tabs.length - 1
    else return

    e.preventDefault()
    setActiveId(tabs[newIndex].id)
    // Перемещаем фокус
    const tabEls = tablistRef.current?.querySelectorAll('[role="tab"]')
    ;(tabEls?.[newIndex] as HTMLElement)?.focus()
  }

  return (
    <div className="tabs">
      <div role="tablist" ref={tablistRef} className="tabs__list">
        {tabs.map((tab, i) => (
          <button
            key={tab.id}
            role="tab"
            id={`tab-${tab.id}`}
            aria-selected={activeId === tab.id}
            aria-controls={`tabpanel-${tab.id}`}
            tabIndex={activeId === tab.id ? 0 : -1}
            onClick={() => setActiveId(tab.id)}
            onKeyDown={e => handleKeyDown(e, i)}
            className={`tabs__tab ${activeId === tab.id ? 'tabs__tab--active' : ''}`}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {tabs.map(tab => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`tabpanel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeId !== tab.id}
          tabIndex={0}
          className="tabs__panel"
        >
          {tab.content}
        </div>
      ))}
    </div>
  )
}

URL-синхронизация табов

Табы, которые не отражаются в URL — ломают кнопку «Назад» и шаринг:

import { useSearchParams } from 'react-router-dom'  // или useRouter в Next.js

function UrlTabs({ tabs }: { tabs: Tab[] }) {
  const [params, setParams] = useSearchParams()
  const activeId = params.get('tab') ?? tabs[0].id

  function setTab(id: string) {
    setParams(prev => { prev.set('tab', id); return prev }, { replace: true })
  }

  return <Tabs tabs={tabs} defaultTab={activeId} onTabChange={setTab} />
}

SEO: контент табов для поисковиков

Содержимое скрытых табов с hidden или display: none индексируется Google, но с меньшим весом. Для SEO-важного контента используем visibility: hidden + height: 0 вместо display: none, или серверный рендер всех панелей:

// SSR: рендерим все панели, скрываем через CSS
<div
  role="tabpanel"
  hidden={activeId !== tab.id}
  // hidden = display:none в браузере, но при SSR HTML-атрибут hidden
  // Google видит контент в HTML
>

Schema.org для FAQ аккордеона

function FaqAccordion({ items }: { items: AccordionItem[] }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: items.map(item => ({
      '@type': 'Question',
      name: item.question,
      acceptedAnswer: {
        '@type': 'Answer',
        text: typeof item.answer === 'string' ? item.answer : '',
      },
    })),
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
      />
      <Accordion items={items} />
    </>
  )
}

Сроки

Простой аккордеон или табы на нативном HTML — 2–3 часа. React-компоненты с анимацией, ARIA, keyboard navigation — 1 день. Система с URL-синхронизацией, Schema.org разметкой и тестами — 1.5 дня.