Raw validated JSON
{
"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"
]
}
}