Tabs and Toolbar

The roving-tabindex family. HTML has no tab widget and no toolbar widget, so unlike most of this library these are written, not borrowed — the same roving implementation the menus use.

import { Tabs, Toolbar, ToggleGroup } from '@apostel/bedrock'
Loading demo…
Source
import { Tabs, Toolbar, ToggleGroup } from '../../src/index'

/**
 * Tabs and Toolbar are the roving-tabindex family: the whole set is one tab
 * stop and arrow keys move within it. HTML has no tab widget, so this is
 * written rather than borrowed — and it is the same roving implementation the
 * menus use.
 */
export default function TabsDemo() {
  return (
    <div style={{ width: '100%' }}>
      <Tabs.Root defaultValue="editor">
        <Tabs.List>
          <Tabs.Trigger value="editor">Editor</Tabs.Trigger>
          <Tabs.Trigger value="preview">Preview</Tabs.Trigger>
          <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value="editor">
          <Toolbar.Root aria-label="Formatting">
            <ToggleGroup.Root type="multiple">
              <ToggleGroup.Item value="bold">Bold</ToggleGroup.Item>
              <ToggleGroup.Item value="italic">Italic</ToggleGroup.Item>
            </ToggleGroup.Root>
            <Toolbar.Separator />
            <Toolbar.Button>Insert link</Toolbar.Button>
          </Toolbar.Root>
        </Tabs.Content>
        <Tabs.Content value="preview">Nothing to preview yet.</Tabs.Content>
        <Tabs.Content value="settings">Settings for this document.</Tabs.Content>
      </Tabs.Root>
    </div>
  )
}

Roving tabindex means the whole set is one tab stop: Tab moves past the group, and arrow keys move within it. That is the APG pattern, and it is what keyboard users expect from a toolbar or a tab strip.

Tabs.Root

Renders a <div>.

prop type notes
defaultValue string Which tab starts selected.
orientation 'horizontal' | 'vertical' Which arrows move focus.
onValueChange (value: string) => void Reports; cannot refuse.
asChild boolean Supported.

For a controlled value, import from @apostel/bedrock/controlled.

Tabs.List / Tabs.Trigger / Tabs.Content

List renders <div role="tablist">, Trigger renders <button role="tab" aria-selected>, Content renders <div role="tabpanel">.

Trigger and Content each take a required value, and are matched by it.

Panel children mount only while their tab is selected, which is what makes an unselected panel's form reset.

Toolbar

part renders notes
Root <div role="toolbar"> Takes orientation.
Button <button>
Link <a> A real anchor: middle-click and copy-link work.
Separator <div role="separator">
ToggleGroup / ToggleItem see form controls Re-exported so a toolbar reads as one component.

Keyboard

key behaviour
Tab Into the group, then out of it. One stop.
ArrowLeft/Right Move within a horizontal group.
ArrowUp/Down Move within a vertical group.
Home/End First and last.

Nested roving containers do not double-step: a key a nested group handles is marked handled, and the outer one stands down. That was a real bug — a toolbar containing a toggle group moved two positions per press — and there is a test for it.

What is not here