Skip to content

Tabs

In the app

Three 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.

PropTypeDefaultDescription
itemsstring[] | { value, label?, count?, disabled? }[]requiredTabs 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.
valuestringuncontrolledControlled selection, with onValueChange.
defaultValuestringitems[0]Initial selection when uncontrolled.
onValueChange(value: string) => voidundefinedFires 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); }