Skip to content
Nizivo UI

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.

Tier: client

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.

Choose a fit.
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.

NameTypeDefaultDescription
aria-labelstring–

Names a chip with no text (a swatch only): it goes on the input, which is what is announced.

childrenReactNode–
disabledboolean–
leadingReactNode–

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 <span style={{ background: '#2e5a3c' }} />.

renderRenderProp<ChipState>–
unavailablebooleanfalse

Out of stock, or otherwise not available: it keeps its colour, gets a strike-through and is described as labels.unavailable. It can still be chosen (to show "Notify me", for example); add disabled to stop that.

valuerequiredstring–

This chip's value: what onValueChange receives and the form submits.

  • Also takes every attribute of <label> except aria-label, htmlFor; children is 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):

NameTypeDefaultDescription
disabledrequiredboolean–
unavailablerequiredboolean–

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.

NameTypeDefaultDescription
childrenReactNode–

Chip elements.

defaultValueM extends true ? readonly string[] : string–

The chip, or with multiple the chips, checked at first (form.reset() returns to them).

disabledboolean–

Disables every chip.

formstring–

The <form> the inputs belong to, when they are outside it.

invalidboolean–

An error message is presented (set by a Field).

labelsPartial<ChipGroupLabels>–
multipleMfalse

Any number of values: native checkboxes under role="group". Without it, one value: native radios under role="radiogroup".

namestring–

The inputs' shared name, and the FormData key (a multiple group submits one entry per checked chip). Without one (and outside a named Field), a generated name groups the inputs.

onValueChange(value: ChipGroupValue<M>, details: ChangeDetails<ChipGroupChangeReason>) => void–

Called when the user checks a chip (or, with multiple, checks or clears one), with every checked value in document order; details.event is the native change event.

renderRenderProp<ChipGroupState>–
requiredM extends true ? never : boolean–

A choice must be made before the form submits. One value only: on checkboxes the browser would require every one.

valueM extends true ? readonly string[] : string | null–

Controlled: the checked chip's value (or null for none); with multiple, every one's.

  • Also takes every attribute of <div> except defaultValue, onChange, role; children is 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):

NameTypeDefaultDescription
disabledrequiredboolean–
invalidrequiredboolean–
multiplerequiredboolean–

Other exports

ChipGroupChangeReason type

import 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"

ChipGroupLabels type

import type { ChipGroupLabels } from '@nizivo/react/chip-group'

ChipGroup's own strings (§16.4): serialisable, English defaults.

NameTypeDefaultDescription
unavailablerequiredstring'Unavailable'

Describes an unavailable chip (out of stock), read after its name; the strike-through is only visual.

ChipGroupProps type

import type { ChipGroupProps } from '@nizivo/react/chip-group'

M is true when multiple is: TypeScript infers it from the prop, so value, defaultValue and onValueChange take arrays exactly when the chips are checkboxes.

The props of ChipGroup, listed above.

ChipGroupState type

import type { ChipGroupState } from '@nizivo/react/chip-group'

The render state of ChipGroup, listed above.

ChipGroupValue type

import type { ChipGroupValue } from '@nizivo/react/chip-group'

What onValueChange receives: the checked value, or with multiple every checked value.

type ChipGroupValue<M extends boolean> = M extends true ? string[] : string

ChipProps type

import type { ChipProps } from '@nizivo/react/chip-group'

The props of Chip, listed above.

ChipState type

import 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.

SlotRendered byData attributes
chip
.nz-chip
Chip
root (takes render); interactive, focus: nested-input; forced colours: border, outline on :has(> input:checked)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
chip-group
.nz-chip-group
ChipGroup
root (takes render)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-invalid (present or absent) ← invalid
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-required (present or absent) ← required
    Mirrors the required attribute
chip-input
.nz-chip-input
Chip
inner element (CSS only)
none
chip-label
.nz-chip-label
Chip
inner element (CSS only)
none
chip-leading
.nz-chip-leading
Chip
inner element (CSS only)
none
chip-strike
.nz-chip-strike
Chip
inner 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.

HookDefaultRead as
--nz-chip-bg--nz-bg-surface
  • background on .nz-chip
--nz-chip-edge-selected--nz-border-brand
  • --_nz-chip-edge on .nz-chip:has( > input:checked)
--nz-chip-fg--nz-text-primary
  • color on .nz-chip
--nz-chip-group-gap--nz-space-2
  • gap on .nz-chip-group
--nz-chip-height--nz-size-control-md
  • block-size on .nz-chip
--nz-chip-leading-size--nz-size-icon-lg
  • block-size on .nz-chip-leading
  • inline-size on .nz-chip-leading
--nz-chip-radius--nz-radius-full
  • border-radius on .nz-chip
--nz-chip-strike--nz-border-strong
  • background on .nz-chip-strike
  • background on .nz-chip-strike

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

KeysAction
TabMoves focus into the group, to the checked chip (one value), or to each chip in turn (multiple values).
ArrowRight, ArrowDownWith one value, checks the next chip and focuses it, skipping disabled ones.
ArrowLeft, ArrowUpWith one value, checks the previous chip and focuses it, skipping disabled ones.
SpaceChecks 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.

On this page