@spark-web/tabs
Version:
--- title: Tabs storybookPath: page-layout-tabs--default isExperimentalPackage: true ---
138 lines (109 loc) • 4.2 kB
Markdown
# @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