Collection controls
Four controls for choosing among a set, moving through a set, editing a set, and showing where you
are in one: ToggleGroup, Tabs, TagGroup and Breadcrumbs. Each is a thin Cratis surface over
its React Aria equivalent, so the keyboard behaviour and the roles come from the foundation and the
appearance is entirely yours through parts.
Choosing one: ToggleGroup or Tabs
Section titled “Choosing one: ToggleGroup or Tabs”These two look alike and mean different things, so pick by what the choice does:
ToggleGroup | Tabs | |
|---|---|---|
| The choice is | a value — a period, a mode, a filter | a view — which panel of content is shown |
| Roles | radiogroup / radio | tablist / tab / tabpanel |
| Arrow keys | move focus; Space or Enter commits | move the selection, and the panel follows |
| Belongs in a form | yes — it is an input | no |
The distinction matters to a screen-reader user, who is told which of the two they have. A segmented
control built out of tablist announces a panel switcher that switches nothing.
ToggleGroup’s arrow keys move focus rather than selecting, because a segmented control often drives
a query: arrowing from Day to Month must not run the month query on the way past.
<ToggleGroup options={[ { value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }, ]} value={period} onChange={setPeriod} aria-labelledby='period-label'/>A group has no single labelable element, so an external <label> associates with
aria-labelledby rather than htmlFor. invalid and aria-describedby wire it to a field’s error
text.
<Tabs tabs={[ { id: 'open', label: 'Open', content: <OpenRequests /> }, { id: 'closed', label: 'Closed', content: <ClosedRequests /> }, ]} value={tab} onChange={setTab} aria-label='Requests'/>Every tab owns a panel and is wired to it through aria-controls. That is why content is required:
the selected tab names its panel, so a tab set without panels would point at an element that is not
there. When the choice drives a view rendered elsewhere it is a value, not a panel — use
ToggleGroup, which tells assistive technology exactly that.
Editing a set: TagGroup
Section titled “Editing a set: TagGroup”A group of removable values with an optional text entry — the control behind a tag or chips field.
<TagGroup value={skills} onChange={setSkills} placeholder='Add a skill…' removeLabel={(skill) => `Remove ${skill}`} aria-labelledby='skills-label'/>| Key | Does |
|---|---|
Enter, or any configured separators character | commits the typed value |
Backspace on an empty entry | removes the last value |
| Arrow keys on a tag | moves between tags |
Backspace or Delete on a tag | removes that tag |
| paste containing a separator | splits and adds each value |
Duplicates are refused unless allowDuplicates is set. editable={false} drops the entry and leaves
a read-only set of removable tags. Give removeLabel a localized builder — the remove buttons are
the one place this control needs words of its own — and removeIcon your icon set’s close icon,
which replaces the default × without touching the button or its accessible name.
Showing where you are: Breadcrumbs
Section titled “Showing where you are: Breadcrumbs”<Breadcrumbs aria-label='Breadcrumb' items={[ { label: 'Requests', href: '/requests' }, { label: 'Equinor ASA', href: '/requests/equinor' }, { label: 'FRP-1284' }, ]}/>The last item is the current page: it carries aria-current="page", has no destination and is
out of the tab order. That is what tells a screen-reader user where the trail ends, and it is why
every segment renders as a link — React Aria hands the current state down to the last one, and a
segment rendered as plain text would lose it.
Pass onNavigate instead of href when a router owns navigation. separator takes any decorative
node and defaults to a slash; separators are hidden from assistive technology either way.
Styling
Section titled “Styling”Every control is styled through its parts — see the pass-through cheat sheet:
| Control | Parts |
|---|---|
ToggleGroup | root, option, icon, label |
Tabs | root, list, tab, panel |
TagGroup | root, list, tag, remove, input |
Breadcrumbs | root, item, link, separator |
The selected option, tab, current breadcrumb and their disabled/invalid states are exposed as
data-selected, data-disabled and data-invalid, so a stylesheet never needs to know the
component’s internals.