Skip to content

Content Switcher

A short row of two to four options that switch the view below it. Three sizes, an on-colour version, and one option always chosen.

Version 0.4.1 ·

Content Switcher

Installation

bunx --bun shadcn@latest add @dga/content-switcher

Usage

page.tsx
tsx
import { useState } from "react";
import { ContentSwitcher, ContentSwitcherItem } from "@/components/ui/content-switcher";

export function PeriodSwitcher() {
  const [period, setPeriod] = useState("week");
  return (
    <ContentSwitcher value={period} onValueChange={setPeriod} aria-label="Period">
      <ContentSwitcherItem value="day">Day</ContentSwitcherItem>
      <ContentSwitcherItem value="week">Week</ContentSwitcherItem>
      <ContentSwitcherItem value="month">Month</ContentSwitcherItem>
    </ContentSwitcher>
  );
}

Use a Content Switcher to change how one set of content is shown: a day, week or month view; a list or a map. All options show the same kind of thing. For separate sections of content use Tabs; for a choice that is saved in a form use Radio. Keep two to four short labels. Name the group with aria-label, and put the content it controls right under it.

Anatomy

  • Switcher (role="group"): the track, neutral-100 with a 1px border. 32, 40 or 48px high.
  • Option (a toggle button with aria-pressed): a label and an optional icon. The chosen option has the primary fill and a white label.

Examples

Sizes

size is sm, md (default) or lg. The three switchers share one value here.

Sizes

On colour

onColor is for a brand fill: a translucent black track and a white chosen option.

On colour

Disabled, icons and controlled

A disabled option stays visible and cannot be chosen. Put an icon first in an option when it helps, and pass value with onValueChange to own the selection.

Disabled, icons and controlled

RTL

The first option is on the right and the Left and Right arrows swap. The switcher is as wide as its options; give it className="w-full" to fill a row.

LTR (English)
RTL (Arabic)

Accessibility

KeyAction
TabMoves focus into the switcher (to the chosen option), then out of it.
ArrowRightMoves focus to the next option (the previous one in Arabic).
ArrowLeftMoves focus to the previous option (the next one in Arabic).
EnterChooses the focused option.
SpaceChooses the focused option.
  • Built on Base UI Toggle Group: a group of buttons with aria-pressed, one of them in the tab order (roving focus). Choosing the pressed option again does nothing, so one option is always chosen.
  • The chosen option is shown by the fill and by aria-pressed. The label is 4.5:1 on the track, the chosen fill 3:1 against it, in light and dark. Focus is a 2px outline inside the option.
  • If the content below changes, announce it: put it in a region with aria-live="polite" or move focus to it.

API Reference

ContentSwitcher

A short row of two to four options that switch the view below it (DGA: Content Switcher). One option is always chosen. One option is in the tab order; the arrow keys move between options (Left and Right swap on Arabic pages). Name it with `aria-label`.

ContentSwitcher props
PropTypeDefault
"sm" | "md" | "lg"

Height: `sm` 32px, `md` 40px, `lg` 48px.

"md"
boolean

For brand fills (`background-primary`, the nav header).

false
string

The chosen option (controlled).

No default
string

The option chosen at first (uncontrolled). The first option if you leave it out.

No default
(value: string) => void

Called with the new value. One option is always chosen, so it is never empty.

No default

Also accepts every prop of the Base UI Toggle (opens in a new tab).

ContentSwitcherItem

One option. Its `value` is what ContentSwitcher reports. Put an icon first if you need one.

ContentSwitcherItem props
PropTypeDefault
string

A unique string that identifies the toggle when used inside a toggle group. What this option is called in code.

No default
boolean

Whether the component should ignore user interaction.

No default

Also accepts every prop of the Base UI Toggle (opens in a new tab).

Deviations from DGA

  • Figma has two or three items with a white label and an OnColor variant; the CSS only sets margin: 10px. The track and the chosen fill are designed here from the button tokens.
  • On a brand fill the track is a translucent black, not white: white lightens the fill and drops the white label to 3.34:1 (2.81:1 pressed).
ChangeReason
Content Switcher: design and on-colourWhite on a white-lightened brand fill is 3.34:1 (down to 2.81:1 pressed); a darkened track keeps 4.5:1 or better, as the on-colour buttons do.

View all 1 deviations for this component

Contrast

40 pairs checked · all pass WCAG 2.1 AAShow 14 states
Variant / stateLightDark
option text / default16.44:1 AAA14.42:1 AAA
focus ring / default16.44:1 3:115.89:1 3:1
option text / hovered16.44:1 AAA9.59:1 AAA
focus ring / hovered16.44:1 3:110.57:1 3:1
option text / pressed14.61:1 AAA6.95:1 AA
focus ring / pressed14.61:1 3:17.66:1 3:1
chosen text4.74:1 AA4.74:1 AA
chosen fill4.31:1 3:13.34:1 3:1
on-colour / option text6.30:1 AA6.30:1 AA
on-colour / option text, hovered7.31:1 AAA7.31:1 AAA
on-colour / option text, pressed8.46:1 AAA8.46:1 AAA
on-colour / chosen text6.60:1 AA6.60:1 AA
on-colour / chosen fill4.74:1 3:14.74:1 3:1
on-colour / focus ring4.74:1 3:14.74:1 3:1

Text needs 4.5:1, icons and focus rings 3:1. Disabled states are exempt. Full contrast report