Components

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.

Available since@crisp-ui-kit/crisp@0.11.0

Installation

npm install @crisp-ui-kit/crisp

Props

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, inset borderSubtle ring, r8) and a single absolutely-positioned 1px bar measured to it (offsetLeft/offsetWidth) is translateX'd/scaled along the baseline; a ResizeObserver keeps 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.

On this page