Design system

Tab toggle

A small pill button for switching views inside a card or panel. You group several to make a switch.

import { TabToggle } from "@/ui";

Each TabToggle is one button. There is no group component: you render two or more side by side, keep the selected one in state, and wrap them in an element with role="tablist".

Switching views

This is the workbook's Output and Formula switch, the pattern the product uses today. Pass the icon as an element at 12px.

import { ChartBar, GearSix } from "@phosphor-icons/react";

const [tab, setTab] = useState("output");

<div role="tablist" aria-label="Step view" style={{ display: "flex", gap: 4 }}>
  <TabToggle
    icon={<ChartBar size={12} />}
    label="Output"
    active={tab === "output"}
    onClick={() => setTab("output")}
  />
  <TabToggle
    icon={<GearSix size={12} />}
    label="Formula"
    active={tab === "formula"}
    onClick={() => setTab("formula")}
  />
</div>

Text only

The icon is optional. Without one, TabToggle works as a compact segmented control, for example to pick a date range on a chart.

const [range, setRange] = useState("30 days");

<div role="tablist" aria-label="Date range" style={{ display: "flex", gap: 4 }}>
  {["7 days", "30 days", "Quarter"].map((r) => (
    <TabToggle key={r} label={r} active={range === r} onClick={() => setRange(r)} />
  ))}
</div>

States

<TabToggle icon={<ChartBar size={12} />} label="Active" active />
<TabToggle icon={<Table size={12} />} label="Default" />
<TabToggle icon={<GearSix size={12} />} label="Disabled" disabled />
StateBackgroundBorderText
Default--color-grey-50transparent--color-grey-600 #52577A
Hover--color-grey-50transparent--color-grey-900 #1F212E
Active--color-white1px --color-grey-600--color-grey-600
Disabled--color-grey-50transparent--color-text-disabled #ADB2CE

All states use 10px medium text on a 16px line, 6px 12px padding, an 8px radius and a 6px gap between icon and label. The icon takes the text colour.

The active toggle keeps the same text colour as the default one. Only the white fill and the grey border mark the selection, so on a white background the difference is the border alone. Place groups on a grey or white surface and check the contrast before using it somewhere new.

The CSS sets font-family: var(--font-family), a token that does not exist. The browser ignores it and the button inherits the page font, which is Poppins in the app.

Props

PropTypeDefaultNotes
labelstringThe text.
iconReactNodeAn element, not a component: icon={<ChartBar size={12} />}.
activebooleanfalseSets the selected style and aria-selected.
disabledbooleanfalseDisables the button and drops onClick.
onClick() => void
classNamestringFor layout only.

No other prop is passed to the <button>, so you cannot add aria-label, aria-controls or id.

Accessibility

  • It renders a native <button> with role="tab" and aria-selected, so it is focusable and works with Enter and Space.
  • The component does not add the role="tablist" parent. Add it yourself with an aria-label, as in the examples. WorkbookChat, the only screen that uses TabToggle, wraps the toggles in a plain <div>, so its tabs have no tablist.
  • There is no arrow-key navigation between toggles and no link to the panel they control. Each toggle is a separate Tab stop.
  • Icon-only toggles cannot be given an accessible name, because aria-label is not passed through. Always include a label.

Do and don't

  • Do use TabToggle for two or three views of the same content inside a card, panel or chart: Output and Formula, Table and Chart, 7 days and 30 days.
  • Do keep exactly one toggle active in a group.
  • Do import from @/ui. WorkbookChat imports from @/ui/components/TabToggle/TabToggle, which works but bypasses the index.
  • Don't use it for page-level sections. Use Tabs.
  • Don't use it as an on and off switch for a setting. Use a Toggle.

On this page