crispmeasured from Attio

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

Showing the overview panel.

Installation

bun add crisp
import "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

PropTypeDefaultDescription
tabsTabItem[]The tabs — each { value, label, icon?, badge?, disabled? }.
leadingReactNodeNon-tab content pinned to the start of the bar (e.g. a ★ button).
valuestringControlled active value. Omit for uncontrolled.
defaultValuestringfirst tabInitial value when uncontrolled.
onValueChange(value: string) => voidFires on selection (click or keyboard).
children(value: string) => ReactNodeRender-prop for the active panel. Omit to render only the bar.
aria-labelstringAccessible 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 a ResizeObserver keeps 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.

On this page