Tabs
In the appThree variants from the app: the segmented control, the underlined line nav and the filled form control. All take counts, all move with the arrow keys.
@varick/ui/components/tabsAdded 26 Aug 2026
Usage
import { Tabs } from "@varick/ui/components/tabs";
export function Example() {
return (
<Tabs
items={["Overview", "Runs", "Errors"]}
defaultValue="Runs"
onValueChange={setView}
/>
);
}Variants
Filled, the form segmented control
<Tabs variant="filled" items={["API key", "OAuth", "Service account"]} />Line variant with counts
<Tabs
variant="line"
items={[
{ value: "all", label: "All", count: 24 },
{ value: "running", label: "Running", count: 9 },
{ value: "archived", label: "Archived", disabled: true },
]}
/>API reference
Every component also accepts className and the native props of its root element.
| Prop | Type | Default | Description |
|---|---|---|---|
| items | string[] | { value, label?, count?, disabled? }[] | required | Tabs in order. A string is both value and label. |
| variant | 'segmented' | 'line' | 'filled' | 'segmented' | Segmented is the navy pill group; line is the underlined nav; filled is the equal-width control on a grey ground, from the connector form. |
| value | string | uncontrolled | Controlled selection, with onValueChange. |
| defaultValue | string | items[0] | Initial selection when uncontrolled. |
| onValueChange | (value: string) => void | undefined | Fires on click and on arrow-key moves. |
Accessibility
- role="tablist" with role="tab" children; the selected tab is aria-selected and the only one in the tab order.
- Left and Right arrows wrap; Home and End jump. Disabled tabs are skipped.
- The component renders tabs only. Pair each tab with a panel that has role="tabpanel" and aria-labelledby.
Styles
The CSS as it appears in the app. The live component above is the same values as Tailwind classes.
.tabs {
display: inline-flex;
align-items: center;
gap: 2px;
background: #FFFFFF;
border: 1px solid var(--hairline);
border-radius: 4px;
padding: 3px;
width: fit-content;
}
.tab {
border-radius: 4px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
color: var(--fg-mid);
}
.tab.active { background: var(--canvas); color: #FFFFFF; box-shadow: 0 1px 2px rgba(0,0,0,0.12); }
.tab .n { font-size: 12px; color: var(--fg-low); }
.tab.active .n { color: rgba(255,255,255,0.75); }
.tabs.line-nav { background: none; border: none; border-bottom: 1px solid var(--hairline-soft); border-radius: 0; padding: 0; }
.tabs.line-nav .tab { padding: 9px 14px; border-radius: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tabs.line-nav .tab.active { background: none; color: var(--canvas); border-bottom-color: var(--canvas); }
.connector-segmented { display: flex; gap: 2px; background: var(--fill); border-radius: 4px; padding: 3px; }
.connector-seg-item { flex: 1; text-align: center; font-size: 13px; color: var(--fg-mid); padding: 9px 8px; border-radius: 4px; }
.connector-seg-item.active { background: #FFFFFF; color: var(--fg-high); font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }