ChipGroup
Pill-shaped choices such as a product's colours or sizes, or filters. Native radios inside labels for one value, or checkboxes with multiple, so it submits, resets and works before hydration. Swatches go in each Chip's leading slot; an unavailable chip keeps its colour and is struck through. Inside a Field, give the Field group.
Import
import { Chip, ChipGroup } from '@nizivo/react/chip-group';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/chip-group.css';Static or interactive
Client. It always ships its client leaf, which wires it to a Field. The server HTML is native radios or checkboxes, so arrows, Space and submission work before hydration. How to choose.
In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.
One value or several
By default the chips are radios under role="radiogroup": value and defaultValue are strings and
onValueChange receives the checked value. With multiple they are checkboxes under role="group":
value and defaultValue are arrays, onValueChange receives every checked value in document
order, and the form submits one entry per checked chip (formData.getAll(name)). required applies
to one value only: on checkboxes the browser would require every one.
Swatches
A product colour is data, not a token. Pass an element with that background in leading, such as
<span style={{ background: '#2e5a3c' }} />, or an image; it fills a 24 px circle with a thin inner
ring, so white and cream stay visible. A chip with a swatch and no text needs an aria-label.
Unavailable or disabled
unavailable (out of stock) keeps the swatch's colour, draws a diagonal strike and describes the
chip as labels.unavailable ("Unavailable"; pass labels={{ unavailable: 'Sold out' }} on the
group). It can still be chosen, so a shopper can pick a sold-out size and be offered "Notify me";
add disabled to stop that. disabled greys the chip out and takes it out of the keyboard order, as
a native disabled input.
No size jump
The edge is an inset ring, not a border: selecting turns it into a 2 px brand ring in the same box, so a chip never grows on select (Figma's grew from 90 × 40 to 92 × 42).
Examples
Basic
Colours with swatches, inside a Field that names the group. One value: native radios, so arrow keys move the choice and the form submits it with no JavaScript. Sand is out of stock: it keeps its colour, is struck through and is described as sold out, and can still be chosen.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Chip, ChipGroup } from '@nizivo/react/chip-group';
/** A product colour is data: the swatch is any element with that background. */
function Swatch({ color }: { color: string }): JSX.Element {
return <span style={{ background: color }} />;
}
export function ChipGroupExample(): JSX.Element {
return (
<Field label="Colour" name="color" group>
<ChipGroup defaultValue="moss" labels={{ unavailable: 'Sold out' }}>
<Chip value="moss" leading={<Swatch color="#2e5a3c" />}>
Moss
</Chip>
<Chip value="clay" leading={<Swatch color="#b5643c" />}>
Clay
</Chip>
<Chip value="sand" leading={<Swatch color="#e9dcc3" />} unavailable>
Sand
</Chip>
<Chip value="slate" leading={<Swatch color="#4a5560" />}>
Slate
</Chip>
</ChipGroup>
</Field>
);
}
Sizes
Sizes: text-only chips at least 48 px wide; selecting one never changes its size.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Chip, ChipGroup } from '@nizivo/react/chip-group';
export function ChipGroupSizesExample(): JSX.Element {
return (
<Field label="Size" name="size" group required>
<ChipGroup defaultValue="m" labels={{ unavailable: 'Sold out' }}>
<Chip value="xs" unavailable>
XS
</Chip>
<Chip value="s">S</Chip>
<Chip value="m">M</Chip>
<Chip value="l">L</Chip>
<Chip value="xl" disabled>
XL
</Chip>
</ChipGroup>
</Field>
);
}
Multiple
Filters: multiple uses checkboxes, so any number can be on, and the form submits one status entry per checked chip. A swatch-only chip is named with aria-label.
import type { JSX } from 'react';
import { Stack } from '@nizivo/react/stack';
import { Chip, ChipGroup } from '@nizivo/react/chip-group';
/** A product colour is data: the swatch is any element with that background. */
function Swatch({ color }: { color: string }): JSX.Element {
return <span style={{ background: color }} />;
}
export function ChipGroupMultipleExample(): JSX.Element {
return (
<Stack gap="4">
<ChipGroup aria-label="Order status" name="status" multiple defaultValue={['paid', 'open']}>
<Chip value="open">Open</Chip>
<Chip value="paid">Paid</Chip>
<Chip value="refunded">Refunded</Chip>
<Chip value="archived">Archived</Chip>
</ChipGroup>
<ChipGroup aria-label="Glaze" name="glaze" defaultValue="white">
<Chip value="white" leading={<Swatch color="#ffffff" />} aria-label="White" />
<Chip value="black" leading={<Swatch color="#1d1d1b" />} aria-label="Black" />
<Chip value="celadon" leading={<Swatch color="#a7c4a0" />} aria-label="Celadon" />
</ChipGroup>
</Stack>
);
}
Error
Required with no default, and an error from the server: every chip not chosen shows it.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Chip, ChipGroup } from '@nizivo/react/chip-group';
export function ChipGroupErrorExample(): JSX.Element {
return (
<Field label="Fit" name="fit" group required error="Choose a fit.">
<ChipGroup>
<Chip value="regular">Regular</Chip>
<Chip value="relaxed">Relaxed</Chip>
<Chip value="slim">Slim</Chip>
</ChipGroup>
</Field>
);
}
Props
Chip
Client component Renders data-slot="chip" with the class nz-chip. Props: ChipProps. Source: src/chip-group/chip-group.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
aria-label | string | – | Names a chip with no text (a swatch only): it goes on the input, which is what is announced. |
children | ReactNode | – | |
disabled | boolean | – | |
leading | ReactNode | – | A swatch or glyph before the text (Figma Type=Colour), in a 24 px circle; decorative. A colour is data: pass an element with that background, such as |
render | RenderProp<ChipState> | – | |
unavailable | boolean | false | Out of stock, or otherwise not available: it keeps its colour, gets a strike-through and is described as |
valuerequired | string | – | This chip's value: what |
- Also takes every attribute of
<label>exceptaria-label,htmlFor;childrenis redeclared above. (Omit<ComponentPropsWithRef<"label">, "htmlFor" | "aria-label">)
render takes an element, or, in client code, a function of the props and this state (ChipState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
unavailablerequired | boolean | – |
ChipGroup
Client component Renders data-slot="chip-group" with the class nz-chip-group. Props: ChipGroupProps<M>. Source: src/chip-group/chip-group.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – |
|
defaultValue | M extends true ? readonly string[] : string | – | The chip, or with |
disabled | boolean | – | Disables every chip. |
form | string | – | The |
invalid | boolean | – | An error message is presented (set by a Field). |
labels | Partial<ChipGroupLabels> | – | |
multiple | M | false | Any number of values: native checkboxes under |
name | string | – | The inputs' shared name, and the |
onValueChange | (value: ChipGroupValue<M>, details: ChangeDetails<ChipGroupChangeReason>) => void | – | Called when the user checks a chip (or, with |
render | RenderProp<ChipGroupState> | – | |
required | M extends true ? never : boolean | – | A choice must be made before the form submits. One value only: on checkboxes the browser would require every one. |
value | M extends true ? readonly string[] : string | null | – | Controlled: the checked chip's |
- Also takes every attribute of
<div>exceptdefaultValue,onChange,role;childrenis redeclared above. (Omit<ComponentPropsWithRef<"div">, "defaultValue" | "onChange" | "role">)
render takes an element, or, in client code, a function of the props and this state (ChipGroupState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
invalidrequired | boolean | – | |
multiplerequired | boolean | – |
Other exports
ChipGroupChangeReasontypeimport type { ChipGroupChangeReason } from '@nizivo/react/chip-group'Why the value changed: the user checked or cleared an input (a pointer or a key).
type ChipGroupChangeReason = "input"ChipGroupLabelstypeimport type { ChipGroupLabels } from '@nizivo/react/chip-group'ChipGroup's own strings (§16.4): serialisable, English defaults.
Name Type Default Description unavailablerequiredstring'Unavailable'Describes an
unavailablechip (out of stock), read after its name; the strike-through is only visual.ChipGroupPropstypeimport type { ChipGroupProps } from '@nizivo/react/chip-group'Mistruewhenmultipleis: TypeScript infers it from the prop, sovalue,defaultValueandonValueChangetake arrays exactly when the chips are checkboxes.The props of
ChipGroup, listed above.ChipGroupStatetypeimport type { ChipGroupState } from '@nizivo/react/chip-group'The render state of
ChipGroup, listed above.ChipGroupValuetypeimport type { ChipGroupValue } from '@nizivo/react/chip-group'What
onValueChangereceives: the checked value, or withmultipleevery checked value.type ChipGroupValue<M extends boolean> = M extends true ? string[] : stringChipPropstypeimport type { ChipProps } from '@nizivo/react/chip-group'The props of
Chip, listed above.ChipStatetypeimport type { ChipState } from '@nizivo/react/chip-group'The render state of
Chip, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
chip.nz-chip | Chiproot (takes render); interactive, focus: nested-input; forced colours: border, outline on :has(> input:checked) |
|
chip-group.nz-chip-group | ChipGrouproot (takes render) |
|
chip-input.nz-chip-input | Chipinner element (CSS only) | none |
chip-label.nz-chip-label | Chipinner element (CSS only) | none |
chip-leading.nz-chip-leading | Chipinner element (CSS only) | none |
chip-strike.nz-chip-strike | Chipinner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-chip-bg | --nz-bg-surface |
|
--nz-chip-edge-selected | --nz-border-brand |
|
--nz-chip-fg | --nz-text-primary |
|
--nz-chip-group-gap | --nz-space-2 |
|
--nz-chip-height | --nz-size-control-md |
|
--nz-chip-leading-size | --nz-size-icon-lg |
|
--nz-chip-radius | --nz-radius-full |
|
--nz-chip-strike | --nz-border-strong |
|
Layers: nz.components. Classes: .nz-chip, .nz-chip-group, .nz-chip-input, .nz-chip-label, .nz-chip-leading, .nz-chip-strike.
Tokens this stylesheet reads (23)
--nz-bg-disabled, --nz-bg-surface, --nz-border-brand, --nz-border-critical, --nz-border-default, --nz-border-strong, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fast, --nz-easing-standard, --nz-font-label-m, --nz-radius-full, --nz-size-control-lg, --nz-size-control-md, --nz-size-icon-lg, --nz-space-2, --nz-space-3, --nz-space-4, --nz-text-disabled, --nz-text-primary, --nz-text-secondary, --nz-tracking-label-m
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
chip: draws the outline when the hidden input inside it has keyboard focus.
In forced-colours mode these states keep a visible system colour: chip (border), chip:has(> input:checked) (outline).
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves focus into the group, to the checked chip (one value), or to each chip in turn (multiple values). |
| ArrowRight, ArrowDown | With one value, checks the next chip and focuses it, skipping disabled ones. |
| ArrowLeft, ArrowUp | With one value, checks the previous chip and focuses it, skipping disabled ones. |
| Space | Checks the focused chip; with multiple values, checks or clears it. |
Source: native <input type="radio"> or <input type="checkbox"> elements inside <label>s (chip-group.client.tsx); its only key handler marks the chip data-focus-visible, because WebKit matches no :focus-visible on a radio reached by an arrow key. The browser lane (test/browser/chip-group.spec.tsx) checks Tab entry, arrows, Space, skipping a disabled chip and reaching an unavailable one.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: Figma node
7275:14018 - Ghost masters: Figma node
753:405
Checkbox
A native checkbox with its label, including the indeterminate (mixed) state. It submits with the form before hydration; inside a Field it is labelled, described and validated by the Field.
ChoiceCard
A choice drawn as cards, such as a saved payment method or a delivery speed. Each ChoiceCard is a label around a real, visible native radio, with slots for a leading mark (a card brand), a second line and trailing content such as a "Default" Badge. ChoiceCardGroup is the radio group; inside a Field, give the Field group, and its error marks the chosen card invalid.