← Todos los specs UI

Contrato del componente

tabs

Patrón WAI-ARIA Tabs (scratch build) — solo la tablist, con roving tabindex, navegación por arrow keys, Home/End y activación automática. El consumidor renderiza sus propios tabpanels en light DOM y togglea visibilidad en dsChange. shadow:false para que los ids de los tabs sean referenciables desde los tabpanels vía aria-labelledby.

Versión
v1.0.0

Props

NombreTipoValoresPredeterminado
itemsstring[]
activestring
orientationenumhorizontal, verticalhorizontal
labelstring

Eventos

NombreCarga útilCondición
dsChangestringfires with the new active tab id whenever selection changes (click, arrow key, Home, End) — including once at mount if the initial `active` prop needed to be reconciled to an enabled tab in items

Tokens

spacing
sm, lg
font
family, size-md

Máquina de estados

Estado inicial: idle

idle

CLICK_TAB
→ idle · dsChange · set_active, focus_stays_on_click_target
ARROW_NEXT
→ idle · dsChange · move_to_next_enabled, set_active, focus_new_active
ARROW_PREV
→ idle · dsChange · move_to_previous_enabled, set_active, focus_new_active
HOME
→ idle · dsChange · move_to_first_enabled, set_active, focus_new_active
END
→ idle · dsChange · move_to_last_enabled, set_active, focus_new_active

Accesibilidad

native_element
div[role=tablist] + button[role=tab] (scratch-built)
apg_pattern
https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
activation_model
automatic — focus move = selection change
keyboard
["Tab from outside → focuses the currently selected tab (only one with tabindex=0)","Tab from selected tab → moves out to the next focusable (typically the active tabpanel with tabindex=0)","Arrow Right (horizontal) / Arrow Down (vertical) → focus and select next enabled tab, wraps at end","Arrow Left (horizontal) / Arrow Up (vertical) → focus and select previous enabled tab, wraps at start","Home → focus and select first enabled tab","End → focus and select last enabled tab","Enter / Space → no-op under automatic activation (native button click behavior preserved)"]
aria
{"tablist_container":"role=tablist, aria-label from `label` prop, aria-orientation=vertical when orientation=vertical (horizontal is the ARIA default, so not emitted)","tab_button":"role=tab, id=item.id, aria-selected=true|false (toggled every dsChange), aria-controls=item.controls || item.id, tabindex=0 (active) or -1 (inactive), disabled native attribute when item.disabled=true"}
focus_lifecycle
{"on_mount":"no auto-focus; tablist is not focused by default","while_inside_tablist":"arrows/Home/End move focus AND selection; focus is bounded to tabs, Tab/Shift+Tab still leave the tablist","on_selection_change_from_keyboard":"component moves programmatic focus to the newly selected tab after re-render (via ref.focus() in componentDidRender), so the visible focus ring tracks selection","on_selection_change_from_click":"click already focuses the target button natively; component does not re-focus"}
consumer_contract
The consumer renders their own tabpanels in light DOM: `<div role=tabpanel id={item.controls||item.id} aria-labelledby={item.id} tabindex=0 hidden={id!==active}>`. shadow:false ensures the tab `id` attributes are reachable from those aria-labelledby references.
notes
Scratch-built per the a11y spec §D ceremony because there is no native <tabs> element. The state machine is enumerated in spec.state_machine — the component synthesizes it, unlike ds-dialog where the browser owns it. Arrow keys wrap around and skip disabled tabs; Home/End land on the first/last enabled tab.
pending_manual_verification
["VoiceOver on macOS: announces 'tab list, N items', tab-of-N position, selected state on selection change, and arrow-key nav announcement","Chromium high-contrast / forced-colors: selected underline remains visible in forced-colors mode"]
JSON validado original
{
  "name": "tabs",
  "version": "1.0.0",
  "props": {
    "items": {
      "type": "string",
      "default": "[]"
    },
    "active": {
      "type": "string"
    },
    "orientation": {
      "type": "enum",
      "values": [
        "horizontal",
        "vertical"
      ],
      "default": "horizontal"
    },
    "label": {
      "type": "string"
    }
  },
  "events": {
    "dsChange": {
      "payload": "string",
      "condition": "fires with the new active tab id whenever selection changes (click, arrow key, Home, End) — including once at mount if the initial `active` prop needed to be reconciled to an enabled tab in items"
    }
  },
  "state_machine": {
    "initial": "idle",
    "states": {
      "idle": {
        "on": {
          "CLICK_TAB": {
            "target": "idle",
            "actions": [
              "set_active",
              "focus_stays_on_click_target"
            ],
            "emit": {
              "event": "dsChange"
            }
          },
          "ARROW_NEXT": {
            "target": "idle",
            "actions": [
              "move_to_next_enabled",
              "set_active",
              "focus_new_active"
            ],
            "emit": {
              "event": "dsChange"
            }
          },
          "ARROW_PREV": {
            "target": "idle",
            "actions": [
              "move_to_previous_enabled",
              "set_active",
              "focus_new_active"
            ],
            "emit": {
              "event": "dsChange"
            }
          },
          "HOME": {
            "target": "idle",
            "actions": [
              "move_to_first_enabled",
              "set_active",
              "focus_new_active"
            ],
            "emit": {
              "event": "dsChange"
            }
          },
          "END": {
            "target": "idle",
            "actions": [
              "move_to_last_enabled",
              "set_active",
              "focus_new_active"
            ],
            "emit": {
              "event": "dsChange"
            }
          }
        }
      }
    }
  },
  "tokens_used": {
    "spacing": [
      "sm",
      "lg"
    ],
    "font": [
      "family",
      "size-md"
    ]
  },
  "a11y": {
    "native_element": "div[role=tablist] + button[role=tab] (scratch-built)",
    "apg_pattern": "https://www.w3.org/WAI/ARIA/apg/patterns/tabs/",
    "activation_model": "automatic — focus move = selection change",
    "keyboard": [
      "Tab from outside → focuses the currently selected tab (only one with tabindex=0)",
      "Tab from selected tab → moves out to the next focusable (typically the active tabpanel with tabindex=0)",
      "Arrow Right (horizontal) / Arrow Down (vertical) → focus and select next enabled tab, wraps at end",
      "Arrow Left (horizontal) / Arrow Up (vertical) → focus and select previous enabled tab, wraps at start",
      "Home → focus and select first enabled tab",
      "End → focus and select last enabled tab",
      "Enter / Space → no-op under automatic activation (native button click behavior preserved)"
    ],
    "aria": {
      "tablist_container": "role=tablist, aria-label from `label` prop, aria-orientation=vertical when orientation=vertical (horizontal is the ARIA default, so not emitted)",
      "tab_button": "role=tab, id=item.id, aria-selected=true|false (toggled every dsChange), aria-controls=item.controls || item.id, tabindex=0 (active) or -1 (inactive), disabled native attribute when item.disabled=true"
    },
    "focus_lifecycle": {
      "on_mount": "no auto-focus; tablist is not focused by default",
      "while_inside_tablist": "arrows/Home/End move focus AND selection; focus is bounded to tabs, Tab/Shift+Tab still leave the tablist",
      "on_selection_change_from_keyboard": "component moves programmatic focus to the newly selected tab after re-render (via ref.focus() in componentDidRender), so the visible focus ring tracks selection",
      "on_selection_change_from_click": "click already focuses the target button natively; component does not re-focus"
    },
    "consumer_contract": "The consumer renders their own tabpanels in light DOM: `<div role=tabpanel id={item.controls||item.id} aria-labelledby={item.id} tabindex=0 hidden={id!==active}>`. shadow:false ensures the tab `id` attributes are reachable from those aria-labelledby references.",
    "notes": "Scratch-built per the a11y spec §D ceremony because there is no native <tabs> element. The state machine is enumerated in spec.state_machine — the component synthesizes it, unlike ds-dialog where the browser owns it. Arrow keys wrap around and skip disabled tabs; Home/End land on the first/last enabled tab.",
    "pending_manual_verification": [
      "VoiceOver on macOS: announces 'tab list, N items', tab-of-N position, selected state on selection change, and arrow-key nav announcement",
      "Chromium high-contrast / forced-colors: selected underline remains visible in forced-colors mode"
    ]
  }
}