Tabs
A row of text tabs over a sliding ink underline — Attio's record-view tab bar.
Attio's record-view tab bar: segmented tab buttons with a 16px leading icon + optional count badge (Emails · 0). The active tab is a light pill (raised fill + subtle ring, r8) with a 2px ink underline that slides along the baseline; inactive tabs are muted and fill to a subtle pill on hover. A leading slot pins non-tab content (e.g. a favourite ★) to the start. Keyboard model is roving-tabindex — ←/→ move and activate, Home/End jump — with activation following focus, matching Attio.
Preview
Installation
bun add crispimport "crisp/styles.css";
import { Tabs } from "crisp";Usage
Tabs renders the bar and, via a render-prop child, the active panel. It's uncontrolled by default (defaultValue) or controlled with value + onValueChange.
import { Tabs } from "crisp";
<Tabs
tabs={[
{ value: "overview", label: "Overview" },
{ value: "emails", label: "Emails", badge: 0 },
]}
defaultValue="overview"
aria-label="Record"
>
{(active) => (active === "overview" ? <Overview /> : <Emails />)}
</Tabs>;Omit the child to render just the tab bar (drive the content yourself from onValueChange).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | TabItem[] | — | The tabs — each { value, label, icon?, badge?, disabled? }. |
leading | ReactNode | — | Non-tab content pinned to the start of the bar (e.g. a ★ button). |
value | string | — | Controlled active value. Omit for uncontrolled. |
defaultValue | string | first tab | Initial value when uncontrolled. |
onValueChange | (value: string) => void | — | Fires on selection (click or keyboard). |
children | (value: string) => ReactNode | — | Render-prop for the active panel. Omit to render only the bar. |
aria-label | string | — | Accessible name for the tablist. |
Notes
- Active pill + sliding underline. The active tab fills to a raised pill (subtle ring,
r8); a single absolutely-positioned 2px bar is measured to the active tab (offsetLeft/offsetWidth),translateX'd/scaled to it along the baseline, and aResizeObserverkeeps it aligned when the bar reflows. - Keyboard. Roving
tabIndex(only the active tab is focusable); ←/→ wrap around enabled tabs and activate immediately, Home/End jump to the ends. - Badges are for counts (Emails · 0); they inherit the tab's muted/inked colour.
Used as the main-area tab bar in the Record Page.