Components

Toggle group

A compound of 2+ Toggles that share exclusivity (type='single') or a multi-select surface (type='multiple'). Built on Radix Toggle Group. Every item shares the exact Toggle chrome via toggleVariants — variant (default / outline) and size (sm / default / lg) apply to the whole group through a Context. Two layouts: the default 'gap' shape (gap-1 between items, each rounded independently) and the Yuno-canonical 'attached' shape (items shoulder-to-shoulder with -mr-px overlap, only outer corners rounded — a segmented control).

Updated Jul 18, 2026 by Leonardo Posada

Anatomy

One primitive with two parts. The Root owns the type (single / multiple), the shared variant/size/attached props and the group value. Each Item is a Toggle button wired to a specific value. Radix handles focus roving, ARIA and value bookkeeping.

  1. 1
    ToggleGroup (Root)

    Radix Root. type='single' returns string | undefined; type='multiple' returns string[]. Owns variant, size and attached — every child Item pulls them from Context.

  2. 2
    ToggleGroupItem

    One toggle button. Requires a unique value string. Reads variant/size/attached from the parent Context so you never have to pass them per-item.

  3. 3
    Layout: gap (default)

    gap-1 between items, each Toggle keeps its full rounded-md and looks like a standalone chip. Best for filter chips and dense toolbars where the group should read as separate pressable buttons.

  4. 4
    Layout: attached (segmented)

    gap-0 with -mr-px overlap on every item except the last, only the outer corners rounded. The 'pill divided in segments' look. Best for exclusive one-of-two/three selections (period pickers, view switchers, Live/Test).

Variants

Two selection modes × two variants × three sizes × two layouts. Pick the mode first (single vs multiple), then let the shape of the group tell the user how to read it.

type='single' (exclusive)

Exclusive one-of-N. value is string | undefined.

<ToggleGroup type="single" defaultValue="a">
  <ToggleGroupItem value="a">A</ToggleGroupItem>
  <ToggleGroupItem value="b">B</ToggleGroupItem>
</ToggleGroup>
type='multiple' (co-select)

Co-select. value is string[].

<ToggleGroup type="multiple" defaultValue={["a", "c"]}>
  <ToggleGroupItem value="a">A</ToggleGroupItem>
  <ToggleGroupItem value="b">B</ToggleGroupItem>
  <ToggleGroupItem value="c">C</ToggleGroupItem>
</ToggleGroup>
Layout: gap (chips)

Default. gap-1 between items; each Toggle keeps its own rounded-md.

<ToggleGroup type="single" variant="outline" defaultValue="grid">
  <ToggleGroupItem value="grid" aria-label="Grid"><GridFour /></ToggleGroupItem>
  <ToggleGroupItem value="list" aria-label="List"><List /></ToggleGroupItem>
</ToggleGroup>
Layout: attached (segmented)

Segmented. Items overlap borders via -mr-px; only outer corners rounded. Pass attached on the Root.

<ToggleGroup type="single" variant="outline" attached defaultValue="24h">
  <ToggleGroupItem value="24h">Last 24 hours</ToggleGroupItem>
  <ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
</ToggleGroup>
Sizes
<ToggleGroup size="sm" />
<ToggleGroup size="default" />
<ToggleGroup size="lg" />

States

Every state on ToggleGroup is inherited from Toggle — idle / hover / pressed (data-[state=on]) / focus / disabled. In the attached layout the pressed item gets z-10 automatically so its border sits on top of its neighbours; the focus ring also goes z-10 so it never clips behind an adjacent item.

When to reach for Tabs, Tabs Pill, ToggleGroup single or ToggleGroup multiple
These four shapes look alike but do different jobs. Tabs (underline, Yuno default): page-level structure — transitioning between sections that show different content. Ex: 'Transactions / Refunds / Chargebacks' above a Table, 'Overview / Rules / Blocklists' in Risk Conditions. The click loads a different content pane. Tabs Pill (segmented): inline inside a Card or KPI, very compact, to pick ONE view of related data. The picker lives next to or inside the content that changes — not in its own zone. Ex: on a KPI Card, 'Daily / Weekly / Monthly' at the top-right; on a Chart, 'Volume / Count' at the top. The click changes WHICH dataset shows in that same space. ToggleGroup single (attached outline): filter / setting / mode that applies over data ALREADY visible (doesn't replace it, reshapes it). Ex: 'Last 24h / 7d / 30d' above a Chart, 'All / Missed' above a list, 'Grid / List / Rows' density picker. The click changes HOW the data is shown, not WHICH data. ToggleGroup multiple: row of include/exclude filter chips above a Table. Ex: 'Credit card / Debit card / Boleto / PIX' to filter payment methods, 'Success / Failed / Pending' for status. The click turns filters ON/OFF in a query. Pocket rule: if the click swaps the whole pane → Tabs; if the click changes WHICH dataset shows in the same pane → Tabs Pill; if the click filters or reshapes the same dataset → ToggleGroup single; if the click toggles chips ON/OFF in a query → ToggleGroup multiple. RadioGroup and Checkbox stay for form fields where the shape needs proper radio / checkbox semantics + labels.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

View mode picker (single, gap, icons)

The canonical 3+ icon-only picker for view density (grid / list / rows). type='single' + variant='outline' + defaultValue='grid'. Each Item wraps a Phosphor icon size-4 weight=light. Give each Item an aria-label so the option is discoverable without the icon.

const [value, setValue] = React.useState("grid");

<ToggleGroup
  type="single"
  variant="outline"
  value={value}
  onValueChange={(v) => v && setValue(v)}
>
  <ToggleGroupItem value="grid" aria-label="Grid view"><GridFour weight="light" className="size-4" /></ToggleGroupItem>
  <ToggleGroupItem value="list" aria-label="List view"><List weight="light" className="size-4" /></ToggleGroupItem>
  <ToggleGroupItem value="rows" aria-label="Rows view"><Rows weight="light" className="size-4" /></ToggleGroupItem>
</ToggleGroup>
Segmented period selector (single, attached, text)

The Yuno canonical 'Last 24 hours / Last 7 days' filter. type='single' + variant='outline' + attached. Text-only Items. Pass a controlled value + onValueChange to fire the query when the period changes. Since it's exclusive, the group MUST always have exactly one active item — never leave it empty.

const [value, setValue] = React.useState("24h");

<ToggleGroup
  type="single"
  variant="outline"
  attached
  value={value}
  onValueChange={(v) => v && setValue(v)}
>
  <ToggleGroupItem value="24h">Last 24 hours</ToggleGroupItem>
  <ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
  <ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
</ToggleGroup>
Multi-select filter chips (multiple, gap, text)

A row of chips above a Table where each chip toggles an include/exclude filter. type='multiple' + variant='outline'. value is a string[], defaultValue starts with the chips that should be on. Order chips by frequency of use, not alphabetically.

const [values, setValues] = React.useState(["cc", "boleto"]);

<ToggleGroup
  type="multiple"
  variant="outline"
  value={values}
  onValueChange={setValues}
>
  <ToggleGroupItem value="cc">Credit card</ToggleGroupItem>
  <ToggleGroupItem value="dc">Debit card</ToggleGroupItem>
  <ToggleGroupItem value="boleto">Boleto</ToggleGroupItem>
  <ToggleGroupItem value="pix">PIX</ToggleGroupItem>
</ToggleGroup>
Split filter (single, attached full-width)

The Yuno 'All / Missed' segmented control: two Items that share a fixed width and each take 50%. Wrap the group in a container with a fixed w-* and add className='[&>*]:flex-1' on the group so children stretch. Great for detail-view secondary filters.

const [value, setValue] = React.useState("all");

<ToggleGroup
  type="single"
  variant="outline"
  attached
  value={value}
  onValueChange={(v) => v && setValue(v)}
  className="w-40 [&>*]:flex-1"
>
  <ToggleGroupItem value="all">All</ToggleGroupItem>
  <ToggleGroupItem value="missed">Missed</ToggleGroupItem>
</ToggleGroup>
Text formatting (multiple, gap, icons)

Bold / Italic / Underline in an editor toolbar. type='multiple' (all three can be on together). Wrap each Item in a Tooltip since they're icon-only. Same Phosphor light→fill swap you'd use on a single Toggle.

const [values, setValues] = React.useState(["bold"]);

<ToggleGroup type="multiple" value={values} onValueChange={setValues}>
  <Tooltip>
    <TooltipTrigger asChild>
      <ToggleGroupItem value="bold" aria-label="Bold">
        <TextB weight={values.includes("bold") ? "fill" : "light"} className="size-4" />
      </ToggleGroupItem>
    </TooltipTrigger>
    <TooltipContent>Bold</TooltipContent>
  </Tooltip>
  {/* italic, underline… */}
</ToggleGroup>

Import

Two imports from @/components/ui/toggle-group. Both share the Toggle atom's chrome, so any change to toggleVariants flows here automatically.

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";

Props

ToggleGroup wraps Radix Root and forwards every prop. The Yuno-specific surface is variant / size (from Toggle) + attached (the segmented layout). Each Item forwards Radix Item props and pulls chrome from the parent Context.

PropTypeDefaultDescription
ToggleGroup.type"single" | "multiple"Required. 'single' → value is string | undefined (exclusive). 'multiple' → value is string[] (co-select).
ToggleGroup.value / defaultValuestring | string[]Controlled value + defaultValue. Match the shape to type. Always seed a default — never let the group render empty.
ToggleGroup.onValueChange(value: string | string[]) => voidFires on every selection change. In single mode Radix can pass an empty string when the user deselects — guard with `if (v) setValue(v)` if you want to keep the current value.
ToggleGroup.variant"default" | "outline""default"Forwarded to every Item via Context. Same values as Toggle.variant.
ToggleGroup.size"sm" | "default" | "lg""default"Forwarded to every Item via Context. Same values as Toggle.size.
ToggleGroup.attachedbooleanfalseYuno-specific. When true, the group renders as a segmented control (gap-0 + -mr-px overlap on every item except the last, only outer corners rounded). Pressed and focus items automatically get z-10 so their border/ring stay on top.
ToggleGroup.disabledbooleanfalseDisables every Item. Individual disabled state is also supported via ToggleGroupItem.disabled.
ToggleGroup.rovingFocusbooleantrueRadix roving focus. Left/Right cycle between items. Turn off only if you're wiring focus manually.
ToggleGroup.orientation"horizontal" | "vertical""horizontal"Radix orientation for arrow-key handling. Yuno uses horizontal only.
ToggleGroupItem.valuestringRequired. Must be unique inside the group. Never reuse.
ToggleGroupItem.disabledbooleanfalseDisables this Item only. Useful when a chip depends on a feature flag.
ToggleGroupItem.aria-labelstringRequired on icon-only Items. Describes the option ('Grid view', 'Bold', 'Last 24 hours').
ToggleGroupItem.classNamestringMerged onto the button via cn(). Layout tweaks only — don't override the pressed / hover surfaces.

When to use

  • Exclusive view mode / density picker (grid / list / rows).
  • Period selector for a Chart / Table (Last 24 hours / Last 7 days / Last 30 days).
  • Include / exclude filter chips at the top of a Table (type='multiple').
  • Text formatting bar in an editor (Bold / Italic / Underline).
  • Segmented one-of-two split filter ('All / Missed', 'Live / Test').

When not to use

  • Navigation between panels of content — use Tabs.
  • Form-field picks (radio / checkbox groups) — use RadioGroup / Checkbox, they carry the right ARIA + label semantics.
  • 5+ options in a picker — Select or Command scales; a row of toggles doesn't.
  • Actions that fire without state (Save, Delete) — Button.

Usage

Do
  • Always pass a defaultValue (single) or a non-empty defaultValue array (multiple) — never let the group render empty.
  • Use attached + outline for exclusive segmented controls (period / view mode / Live vs Test).
  • Use gap for chip-style filters where each option reads as its own pressable button.
  • Give every icon-only Item an aria-label and wrap it in a Tooltip.
  • Use controlled value + onValueChange whenever the group's value drives fetching or navigation.
Don't
  • Don't use type='single' as a router — that's Tabs' job.
  • Don't set the same value on two Items — Radix will treat them as one.
  • Don't mix icon-only and text Items in the same group — the visual weight breaks apart.
  • Don't rely on the initial value being empty — always seed a default so the surface never looks broken.
  • Don't add gap AND attached — pick one layout per group.

Related

Cross-links to atoms and patterns you may reach for next.

  • ToggleThe single-button atom this compound is built on. Reach for Toggle when you need one stateful button, ToggleGroup when you need 2+ that share exclusivity or multi-select.
  • TabsFor navigating between panels of related content. If the picker CHANGES what the user sees (not just filters it), Tabs is usually the answer.
  • Radio groupFor form-field exclusive picks with proper radio semantics + labels. ToggleGroup single is button-shaped; RadioGroup is form-shaped.
  • CheckboxFor form-field multi-select. ToggleGroup multiple is only for chip-style filter rows above a Table.
  • TooltipWraps icon-only Items so keyboard users know what each toggle does.