UNPKG

@spark-web/tabs

Version:

--- title: Tabs storybookPath: page-layout-tabs--default isExperimentalPackage: true ---

138 lines (109 loc) 4.2 kB
# @spark-web/tabs — AI Context ## What this is A tab interface built on Radix UI Tabs. Renders a horizontal tab bar with associated content panels. Tabs activate on focus by default. ## Exports - `Tabs` — root container - `TabList` — the horizontal row of tab triggers - `Tab` — individual tab trigger (must be a direct child of `TabList`) - `TabPanels` — container for panels (renders a `Divider` above by default) - `TabPanel` — individual panel (must be a direct child of `TabPanels`) Tabs and panels are matched by position — `Tab[0]` corresponds to `TabPanel[0]`. There are no explicit `value` props to wire up. ## Usage ```tsx import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@spark-web/tabs'; <Tabs> <TabList> <Tab>Email</Tab> <Tab>SMS</Tab> </TabList> <TabPanels> <TabPanel> <EmailHistoryTable /> </TabPanel> <TabPanel> <SmsHistoryTable /> </TabPanel> </TabPanels> </Tabs>; ``` ## Props ### `Tabs` | Prop | Type | Default | Notes | | ---------------- | ------------------------- | ------------- | --------------------------------------------- | | `defaultIndex` | `number` | `0` | Index of initially active tab | | `activationMode` | `'automatic' \| 'manual'` | `'automatic'` | Automatic activates on focus; manual on click | ### `Tab` | Prop | Type | Notes | | ---------- | ----------------------------- | --------------------------------- | | `disabled` | `boolean` | Prevents interaction with the tab | | `paddingY` | `keyof SparkTheme['spacing']` | Vertical padding around the tab | ### `TabPanels` | Prop | Type | Default | Notes | | ------------- | --------- | ------- | ---------------------------------------------- | | `showDivider` | `boolean` | `true` | Renders a `Divider` above the panels when true | ## Internal admin surface: background override `@spark-web/tabs@5.2.1` sets a non-transparent `background: surface` on tab buttons. This conflicts with the neutral background of `SectionCard` in admin surfaces, producing a visible white box behind each tab trigger. Override with the following CSS wrapper whenever `Tabs` is used inside a `SectionCard`: ```tsx import { css } from '@emotion/css'; import { Box } from '@spark-web/box'; <Box className={css({ '> div > div > div > button': { backgroundColor: 'transparent !important', letterSpacing: 0, '&:focus-visible': { outline: '2px solid currentColor', outlineOffset: 2, }, }, })} > <Tabs>...</Tabs> </Box>; ``` This override is required in admin surfaces — never omit it when rendering `Tabs` inside a `SectionCard`. ## Dynamic tab construction Build the tabs array from data before rendering, and return `null` if no tabs can be constructed: ```tsx const tabs = useMemo(() => { const result: { label: string; content: ReactNode }[] = []; if (hasEmail) result.push({ label: 'Email', content: <EmailHistoryTable /> }); if (hasMobile) result.push({ label: 'SMS', content: <SmsHistoryTable /> }); return result; }, [hasEmail, hasMobile]); if (tabs.length === 0) return null; return ( <Tabs> <TabList> {tabs.map(tab => ( <Tab key={tab.label}>{tab.label}</Tab> ))} </TabList> <TabPanels> {tabs.map(tab => ( <TabPanel key={tab.label}>{tab.content}</TabPanel> ))} </TabPanels> </Tabs> ); ``` ## Do NOTs - NEVER render `Tabs` inside `SectionCard` without the `backgroundColor: 'transparent !important'` override — the surface background will bleed through - NEVER omit the null guard when tabs are built dynamically — render `null`, not an empty `Tabs` wrapper - NEVER mix `Tab` with non-`Tab` children inside `TabList` — it throws at runtime - NEVER mix `TabPanel` with non-`TabPanel` children inside `TabPanels` — it throws at runtime - NEVER use `value` strings to match tabs and panels — position-based matching is automatic