"use client" import type { ComponentDocSpec } from "@/lib/design-system/component-doc-types" /** Component-focused UX: peer panels via Radix tablist. Live previews: design-system-previews.tsx */ export const tabsComponentDoc: ComponentDocSpec = { slug: "tabs", summary: "Radix tablist primitive: Tabs, TabsList, TabsTrigger, and TabsContent for one visible panel at a time.", sections: [], anatomy: [ { part: "Tabs", description: "Root context; set orientation horizontal or vertical." }, { part: "TabsList", description: "Tablist chrome; variant default (pill) or line (underline), with default resolving to line in the compact shell. Measures itself and handles its own overflow, so no wrapper is needed." }, { part: "TabsListScrollRegion", description: "Optional. Passes overflow settings to the list it wraps. Only needed to put a class on the scroll region itself, such as page gutters on a full-bleed row." }, { part: "Overflow menu", description: "Rendered by TabsList as the last thing inside the track, as menuitemradio entries. The selected tab is never inside it." }, { part: "Track shell", description: "Wears the list chrome while an overflow trigger shares the row, so the trigger sits inside the track without being a tablist child." }, { part: "TabsTrigger", description: "Selectable tab control; supports icon and TabsCountBadge." }, { part: "TabsTriggerIcon", description: "Marks the glyph in a trigger. Only triggers with this slot may collapse to icon only." }, { part: "TabsTriggerLabel", description: "Marks the text in a trigger. Becomes sr-only when the row runs out of room, so the accessible name survives." }, { part: "TabsContent", description: "Panel mounted for the active value only." }, { part: "TabsCountBadge", description: "Optional numeric chip on a trigger." }, ], api: [ { prop: "orientation", type: "horizontal | vertical", defaultValue: "horizontal", description: "On Tabs root." }, { prop: "variant", type: "default | line", defaultValue: "default", description: "On TabsList. Pill vs underline. The compact shell renders default as line." }, { prop: "pinVariant", type: "boolean", defaultValue: "false", description: "On TabsList. Keeps variant as passed, ignoring shell density. Only for surfaces that demonstrate a variant rather than use one." }, { prop: "ariaLabel", type: "string", defaultValue: "Tabs", description: "On TabsList. Names the scroll region around the row." }, { prop: "overflow", type: "boolean", defaultValue: "true", description: "On TabsList. Set false to keep the row at its natural width and let something else handle overflow." }, { prop: "collapseLabels", type: "boolean", defaultValue: "true", description: "On TabsList. Set false to keep every label and shed whole tabs instead." }, { prop: "overflowMenu", type: "boolean", defaultValue: "true", description: "On TabsList. Set false to scroll the row instead of moving tabs into a menu." }, { prop: "overflowLabel", type: "string", defaultValue: "More tabs", description: "On TabsList. Accessible name and tooltip for the overflow trigger." }, ], ux: { job: "Let users switch between peer content regions on one surface without changing route or losing parent context.", budgets: [ { label: "Visible triggers", value: "≤7", rationale: "Beyond seven, group the content. The row will shed tabs into a menu rather than break, but a menu is not an IA fix." }, { label: "Trigger labels", value: "≤3 words", rationale: "Short parallel names; long labels truncate on narrow viewports." }, ], principles: ["P1", "P2", "P3", "P6", "P13"], modernReferences: [ "Height view tabs (M1, M4)", "Linear issue detail sections (M1, M4)", "Stripe customer record tabs (M4, M11)", ], patternDoc: "apps/web/docs/tabs-pattern.md", rulePath: ".cursor/rules/exxat-tabs-chrome.mdc", whenToUse: [ "Peer sections on one record or card (not sequential steps).", "Two to seven named panels where only one body is visible at a time.", "In-card panel pairs such as chart vs trend (line variant).", ], whenNotToUse: [ "Hub view switching (table / board / dashboard). ViewSegmentedControl on ListPageTemplate.", "Sequential create flows with completed steps. Wizard.", "Theme, chart type, or toolbar mode pickers. FilterChipGroup (muted) or ButtonSegmentedControl.", "Destructive confirmations. AlertDialog.", ], }, guidelines: { do: [ "Compose Tabs → TabsList (inline-flex w-fit) → TabsTrigger → TabsContent.", "Set variant=\"default\" on TabsList for pill chrome; variant=\"line\" for underline panels.", "Leave primary tabs unpinned. The compact shell layout renders a default row as line, which is how the whole variant drops filled chrome; pinVariant opts out and belongs to this catalog only.", "Let TabsList handle overflow. It measures itself, so there is nothing to switch on; tune it with ariaLabel, collapseLabels, overflowMenu, or overflow={false}.", "Build every navigational tab from TabsTriggerIcon + TabsTriggerLabel. That is the row's cheapest rung of degradation: inactive tabs drop to their glyph while the selected one keeps icon plus label, so a narrow row still shows every destination. Pick a glyph that is recognisable alone, reusing whatever the sidebar already gives that concept.", "Omit the icon only when the label is a value rather than a place (chart data slice, period picker, metric switcher). No glyph tells those apart, so let the row shed whole tabs into the menu instead.", "Give every TabsTrigger a value, since that is what the overflow menu selects by.", "Keep TabsCountBadge counts honest and tabular-nums.", ], dont: [ "Pass w-full or flex-1 on TabsList. Default w-fit must stay.", "Use TabsTriggerLabel without TabsTriggerIcon. A label-only tab would collapse to nothing, so it stays expanded instead.", "Ship a navigational tab as a bare text child. It cannot trade its label for a glyph, so the first thing the row does when it runs out of room is hide destinations in a menu.", "Hide a collapsed label with display:none or hidden. sr-only keeps the tab's accessible name.", "Make the overflow trigger a child of the tablist. A tablist may only contain tabs, so TabsList renders it as a sibling and moves the track onto a shell around both.", "Wrap a list in TabsListScrollRegion to get overflow behaviour. It is already there; the wrapper only carries settings and a class for the region.", "Use Radix Tabs for hub saved views. ViewSegmentedControl is radiogroup, not tablist.", "Nest Tabs inside Tabs. Flatten IA or split routes.", ], }, accessibility: [ { principle: "perceivable", criterion: "1.3.1", criterionTitle: "Info and Relationships", level: "A", guidance: "TabsList exposes role=tablist; each TabsTrigger is role=tab; each TabsContent is role=tabpanel linked with aria-labelledby.", }, { principle: "perceivable", criterion: "1.4.1", criterionTitle: "Use of Color", level: "A", guidance: "Selected state must not rely on color alone. Pair active chrome with text weight, underline, or pill fill.", }, { principle: "operable", criterion: "2.1.1", criterionTitle: "Keyboard", level: "A", guidance: "Arrow keys move focus between TabsTrigger; Home and End jump to first and last tab when orientation is horizontal.", }, { principle: "operable", criterion: "2.4.7", criterionTitle: "Focus Visible", level: "AA", guidance: "TabsTrigger shows a visible focus ring; do not remove focus-visible styles.", }, { principle: "operable", criterion: "2.5.8", criterionTitle: "Target Size (Minimum)", level: "AA", guidance: "Triggers meet the 24×24 CSS px floor or have 24px spacing between adjacent targets.", }, { principle: "understandable", criterion: "2.4.6", criterionTitle: "Headings and Labels", level: "AA", guidance: "Every trigger has visible text or aria-label; icon-only triggers need Tip + aria-label with matching copy. A trigger collapsed by the fit ladder keeps its TabsTriggerLabel as sr-only and gains a Tip automatically.", }, { principle: "perceivable", criterion: "1.4.4", criterionTitle: "Resize Text", level: "AA", guidance: "Collapsing is measured, not breakpoint based, so a row that overflows at 200 percent zoom sheds its inactive labels rather than clipping them. The selected tab keeps its label, and never moves into the overflow menu, so the user never loses their place.", }, { principle: "operable", criterion: "2.1.1", criterionTitle: "Keyboard (overflow menu)", level: "A", guidance: "Tabs moved into the overflow menu stay reachable: the trigger is in the tab order and the menu is a standard menu with arrow key and type-ahead support. Entries are menuitemradio, so assistive tech announces which tab is current.", }, { principle: "understandable", criterion: "3.2.4", criterionTitle: "Consistent Identification", level: "AA", guidance: "Tab labels stay stable across visits; do not rename tabs between sessions for the same panel.", }, { principle: "robust", criterion: "4.1.2", criterionTitle: "Name, Role, Value", level: "A", guidance: "Radix sets aria-selected and roving tabindex on triggers; preserve primitive markup when extending styles.", }, ], relatedSlugs: ["horizontal-scroll-region", "view-segmented-control", "wizard", "button-segmented-control", "filter-chip-group", "alert-dialog"], }