Вкладки

Дайте каждому элементу href — и вкладки станут ссылками: по странице на вкладку, без скрипта, с aria-current на активной. Дайте каждому элементу panel — и они превратятся в группу радиокнопок, панели которой подменяются на месте, и скрипта по-прежнему нет.

На статическом сайте обычно правильна форма со ссылками: она даёт каждому виду свой URL и переживает выключенный JavaScript. Берите панели, когда содержимого немного и переключение не должно стоить перехода.

Вкладки-ссылки

Это действительно ссылки: по клику происходит переход. Подчёркивание берётся из active или value на сборке, а не из клика, — каждая страница помечает свою вкладку. Сама по себе вкладка-ссылка на URL не реагирует: для этого переключайтесь на месте панелями ниже.

tabs({
  items: [
    { label: 'Хлебные крошки', href: '/ru/ui/breadcrumbs' },
    { label: 'Вкладки', href: '/ru/ui/tabs', active: true },
    { label: 'Пагинация', href: '/ru/ui/pagination' },
  ],
})

Вкладки с панелями

Вкладка — это <label> для радиокнопки, которую стили убирают с глаз, а показывается панель, идущая за отмеченной радиокнопкой. Страница ничего не импортирует: и переключение, и стрелки между вкладками — это то, что группа радиокнопок умеет сама.

npm install sitelo javascript-to-html
import * as ui from 'sitelo/ui'
sitelo build
tabs({
  value: 'install',
  items: [
    { id: 'install', label: 'Установка', panel: card({ variant: 'flat' }, cardBody(code('npm install sitelo javascript-to-html'))) },
    { id: 'use', label: 'Использование', panel: card({ variant: 'flat' }, cardBody(code("import * as ui from 'sitelo/ui'"))) },
    { id: 'build', label: 'Сборка', panel: card({ variant: 'flat' }, cardBody(code('sitelo build'))) },
  ],
})

Вкладки со ссылкой

Дайте элементам с панелями ещё и href с фрагментом — и вместо радиокнопок будут ссылки: URL называет вкладку, :target её выделяет, показывается идущая за ней панель, а выбор переживает перезагрузку, отправленную ссылку и кнопку «назад». Id стоит на вкладке, а не на панели, потому что браузер подтягивает названное в URL к верхнему краю окна: с id на панели вкладки уехали бы за пределы экрана, на котором вы только что по ним щёлкнули. В документе может быть только один :target, поэтому эта форма — для одного набора вкладок на странице. Саму прокрутку отменить нельзя: перейти по фрагменту и значит сдвинуть окно. Выбрать можно лишь, к чему оно едет и где остановится, — для этого id на вкладке и её scroll-margin-block-start, который задаётся пропом scrollMargin: липкой шапке нужна как минимум её собственная высота.

Настройка

Эта панель — #tab-setup: скопируйте URL, и она вернётся.

Деплой

А эта — #tab-deploy.

tabs({
  items: [
    { id: 'setup', label: 'Настройка', href: '#tab-setup', panel: card({ variant: 'flat' }, cardBody(text({ variant: 'small' }, 'Эта панель — #tab-setup: скопируйте URL, и она вернётся.'))) },
    { id: 'deploy', label: 'Деплой', href: '#tab-deploy', panel: card({ variant: 'flat' }, cardBody(text({ variant: 'small' }, 'А эта — #tab-deploy.'))) },
  ],
})

Пилюли

src/index.ht.js
src/index.ht.ts
src/index.ht.jsx
stack({ gap: 'lg' },
  tabs({
    variant: 'pills',
    items: [
      { label: 'Все', href: '#all', active: true },
      { label: 'Руководства', href: '#guides' },
      { label: 'Примеры', href: '#examples' },
    ],
  }),
  tabs({
    variant: 'pills',
    value: 'js',
    items: [
      { id: 'js', label: 'JavaScript', panel: card({ variant: 'flat' }, cardBody(code('src/index.ht.js'))) },
      { id: 'ts', label: 'TypeScript', panel: card({ variant: 'flat' }, cardBody(code('src/index.ht.ts'))) },
      { id: 'jsx', label: 'JSX', panel: card({ variant: 'flat' }, cardBody(code('src/index.ht.jsx'))) },
    ],
  }),
)

Цвета

stack({ gap: 'lg' },
  tabs({ color: 'primary', items: [{ label: 'Primary', href: '#p', active: true }, { label: 'Другая', href: '#p2' }] }),
  tabs({ color: 'neutral', items: [{ label: 'Neutral', href: '#n', active: true }, { label: 'Другая', href: '#n2' }] }),
  tabs({ color: 'danger', items: [{ label: 'Danger', href: '#d', active: true }, { label: 'Другая', href: '#d2' }] }),
)

Много вкладок

Полоса вкладок прокручивается по горизонтали, а не переносится, поэтому ряд сохраняет форму и на телефоне. Вкладки с панелями, наоборот, переносятся: каждая панель обязана идти за своей вкладкой, так что прокручивать просто нечего.

tabs({
  items: [
    'Обзор', 'Маршруты', 'Данные', 'Ресурсы', 'Изображения', 'Острова', 'TypeScript', 'CLI', 'Развёртывание',
  ].map((label, index) => ({ label, href: '#many-' + index, active: index === 0 })),
})

Отключённая вкладка

Эта работает.

tabs({
  value: 'now',
  items: [
    { id: 'now', label: 'Доступно', panel: card({ variant: 'flat' }, cardBody(text({ variant: 'small' }, 'Эта работает.'))) },
    { id: 'soon', label: 'Скоро', disabled: true, panel: card({ variant: 'flat' }, cardBody('')) },
  ],
})

Доступность

Форма с панелями — это настоящая группа радиокнопок: вкладки суть элементы <label> для радиокнопок с общим name, поэтому скринридер называет, какая из скольких выбрана, а стрелки, Home и End работают без единого загруженного скрипта. Форма со ссылкой — это просто ссылки, и aria-current в ней нет: он был бы записан один раз и стал бы неправдой после первого же клика. Это намеренно не ARIA-tablist: aria-selected пишется один раз, на сервере, и CSS не может сохранять его правдивым по мере кликов. Форма со ссылками тоже не tablist: ссылки, которые ведут на другие страницы, — это ссылки, и приписывать им семантику вкладок значило бы врать о том, что они делают.

Пропсы

ПропТипПо умолчаниюОписание
itemsArray[]Строки или объекты { id, label, href, panel, active, disabled }.
valuestring—Id активного элемента. Иначе берётся active, а затем первый.
variant'underline' | 'pills''underline'Чем помечается активная вкладка.
color'primary' | 'neutral' | 'success' | 'warning' | 'danger''primary'Цвет активной вкладки.
labelstring'Tabs'Доступное имя группы. Только для формы с панелями.
namestringid первого элементаИмя группы радиокнопок. Нужно, только если на странице два набора вкладок с панелями.
hrefstring—У элемента: страница, на которую вести, — или, вместе с panel, фрагмент, который её называет.
scrollMarginSpace'md'Насколько выше вкладки останавливается окно. Только для формы :target.