Tabs
A row of text tabs over a sliding ink underline — a record-view tab bar.
A record-view tab bar: segmented tab buttons with a 16px leading icon + optional count badge (Emails · 0). The active tab has no fill of its own — just the ink label colour — with a 1px 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 the measured reference.
Installation
npm install @crisp-ui-kit/crispProps
Prop
Type
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-ui-kit/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).
trailing
leading pins content to the start of the bar; trailing (crisp#1304) pins controls to the end of the same row, right-aligned and vertically centred, so a page toolbar (filter, display, search) can share the row with the tabs. It is rendered beside the tab list, outside the role="tablist" element: ←/→ still move only between tabs, and Tab order is the tabs, then the trailing controls. The baseline runs under both, the tabs shrink (or collapse into "+N more") before the trailing block does, and omitting the prop leaves the markup unchanged.
<Tabs tabs={tabs} defaultValue="all" trailing={<Toolbar />} aria-label="Tasks" />overflow="scroll"
By default (overflow="collapse"), a tab row that runs out of width hides trailing tabs
behind a "+N more" menu, matching Attio's own record-view tabs. overflow="scroll"
(crisp#908) never hides a tab — the row scrolls horizontally instead, snapping each tab to the
left edge, matching Naver/Kakao's own tab-row pattern (docs/study/korean/). The two modes
are mutually exclusive — "scroll" never computes or renders the "+N more" trigger.
Notes
- Raised pill plus sliding underline. The active tab is a light pill (
#fbfbfb, insetborderSubtlering, r8) and a single absolutely-positioned 1px bar measured to it (offsetLeft/offsetWidth) istranslateX'd/scaled along the baseline; aResizeObserverkeeps it aligned when the bar reflows. Inactive tabs gain the same ring (no fill) on hover. Restored to the measured Attio look after #940 had removed the pill. - 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.