FairGarden Design

Chip

The removable chip: a chosen value with its × in its own hit area. The Combobox's multiple selection and filter UIs that list the active filters share it; the filter chip that toggles a filter is the Toggle's chip variant.

import { Chip } from '@fairgarden-private/design/components/Chip'

The chip is --size-px-7 tall with a 1.5 px --role-rule edge and the label in type-body-ui. onRemove adds the × (close, inline tier) in a 44 px target with its own focus ring; its accessible name defaults to "Remove [label]". A chip without onRemove shows the value alone. Sets wrap; they scroll sideways only inside a rail Scroll Area.

Removable chips

Removable chips

Hovering the × steps the glyph to the next stroke weight; pressing fills its cell with the inverse pair. disabled mutes the label and ×, and draws the edge as fine dots, never a faded chip. On forest the edge and label take the field's one ink.

paper
  • Oak Savanna
  • Wetland
  • Tallgrass Prairie
  • Riparian Forest
  • Members Only
forest
  • Oak Savanna
  • Wetland
  • Tallgrass Prairie
  • Riparian Forest
  • Members Only

4 of 4 filters active

ChipRemovable.tsx
'use client'

import * as React from 'react'
import { Chip } from '@fairgarden-private/design/components/Chip'
import { PresetGround } from '@/components/PresetGround'
import styles from './removable.module.css'

const initial = ['Oak Savanna', 'Wetland', 'Tallgrass Prairie', 'Riparian Forest']

/** Active filters as removable chips; the last one is locked. On paper and on the forest field. */
export function ChipRemovable() {
  const [chosen, setChosen] = React.useState(initial)
  const remove = (value: string) => setChosen((list) => list.filter((item) => item !== value))

  return (
    <div className={styles.stack}>
      {(['paper', 'forest'] as const).map((preset) => (
        <PresetGround key={preset} preset={preset} className={styles.face}>
          <span className={styles.name}>{preset}</span>
          <ul className={styles.row} aria-label="Active filters">
            {chosen.map((value) => (
              <li key={value}>
                <Chip onRemove={() => remove(value)}>{value}</Chip>
              </li>
            ))}
            <li>
              <Chip disabled onRemove={() => undefined} removeLabel="Remove Members Only (locked)">
                Members Only
              </Chip>
            </li>
          </ul>
        </PresetGround>
      ))}
      <p className={styles.status} aria-live="polite">
        {chosen.length} of {initial.length} filters active
        {chosen.length < initial.length ? (
          <>
            {' '}
            <button type="button" className={styles.reset} onClick={() => setChosen(initial)}>
              Reset
            </button>
          </>
        ) : null}
      </p>
    </div>
  )
}

Composing it

A composed component that renders chips on its own Base UI parts passes them through render and removeRender (for example the Combobox's Chip and ChipRemove), or applies chipParts to them, so the look stays in one module.

In print

Chips print as words: the edge and × drop.

API Reference

A removable chip: --size-px-7 tall, a --ds-stroke-1-5 --role-rule edge, the label in type-body-ui and the × (close, inline tier) in a --ds-size-hit target with its own focus ring. It prints as the word.

PropTypeDescription
onRemove
| ((
    event: React.MouseEvent<
      HTMLButtonElement,
      MouseEvent
    >,
  ) => void)
| undefined

Called when the × is pressed. Omitted, the chip renders without its × (a read-only value).

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: edge, label, × and focus rings. Never defaulted; omitted, it inherits the scope [D133].

removeLabel
string | undefined

The בs accessible name. Default “Remove [label]” when children is a string, otherwise “Remove”.

removeRender
ReactElement | undefined

Replaces the × element, e.g. <Combobox.ChipRemove />, which then receives the module’s class, the glyph and the accessible name.

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: accepted for the shared contract; unused. Never defaulted.

disabled
boolean | undefined

The value can’t be removed: muted label and ×, dotted edge. Default false.

children
React.ReactNode | undefined

The chosen value, e.g. “Oak Savanna”. Never truncated in print.

chip
type chip = chip
chipParts

The module’s part classes, for a composed component that renders the chip on its own Base UI parts (the Combobox’s Chip and ChipRemove) and leaves the look to this module (§1.11.1).

type chipParts = { base: string; label: string; remove: string; edge: string; edgeLine: string }
ChipProps

Props for Chip: span props, render, the remove handler and label, and the color axes.

type ChipProps = {
  /** The chosen value, e.g. "Oak Savanna". Never truncated in print. */
  children: React.ReactNode;
  /**
   * Called when the × is pressed. Omitted, the chip renders without its ×
   * (a read-only value).
   */
  onRemove?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
  /**
   * The ×'s accessible name. Default "Remove [label]" when `children` is a
   * string, otherwise "Remove".
   */
  removeLabel?: string;
  /**
   * Replaces the × element, e.g. `<Combobox.ChipRemove />`, which then
   * receives the module's class, the glyph and the accessible name.
   */
  removeRender?: ReactElement;
  /** The value can't be removed: muted label and ×, dotted edge. Default `false`. */
  disabled?: boolean;
  /**
   * Primary Radix scale: edge, label, × and focus rings. Never defaulted;
   * omitted, it inherits the scope [D133].
   */
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /** Secondary Radix scale: accepted for the shared contract; unused. Never defaulted. */
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
}

Specification: DESIGN-SYSTEM.md §10.11 (badge, tag and chip) and §9.4 (the filter chip).