Agent docs

Chip

A small toggleable tag, backed by daisyUI's badge and its style modifiers.

A small toggleable tag, backed by daisyUI's badge and its style modifiers. It always renders a real <button> (or, when removable, a <span> wrapping one), and it communicates SELECTION, never state or naming.

The canon behind it. docs/design-language/03-status-language.md §A1 (03-status-language.md:13, three small-tag species, never interchangeable) and docs/design-language/07-toolbars-and-filters.md §A4 (07-toolbars-and-filters.md:24, a trigger that can only summarize as a count owes the reader removable chips).

Click feedback. Every chip type and variant presses on pointer-down (active:scale-[0.96]) and pops for 280ms when it toggles (animate-chipPop, keyed to the click, cleared on animation end); FieldChip's clickable shape shares the press. Owner ruling doc 59 V3: the feedback is a kit default, never something a page adds. Both are stilled under prefers-reduced-motion.

When to reach for it#

Anything the user can select, deselect, or remove: a filter value shown as a removable pill beside its control, a plan or role tag in a card toolbar, a click-to-navigate pill. Canon doc 3 §A1 draws the line precisely: "three small-tag species, never interchangeable. StatusPill/StatusDot communicate STATE. Chip communicates SELECTION... FieldChip communicates NAMING."

Do not reach for it to show a record's status (queued, running, failed). That is StatusPill or StatusDot. Do not reach for it to name a schema field or a file. That is FieldChip. Using Chip for either is called out in the canon as a defect, not a style preference. Do not reach for it to build a two-option segmented toggle either: the shipped precedent for that shape (walmart-mvp/frontend/src/features/catalog/ItemsView.tsx:62-90, ToggleChip, the "By part type / All items" switch) is a hand-rolled joined button pair, not Chip with selected. As of this pass Chip's own selected prop has no confirmed shipped call site in either app; when a two-way toggle is the actual need, the codebase's own answer has been a bespoke button pair, not this component.

Contract#

Identity:

  • children: the label.
  • presets: ChipPreset | ChipPreset[] (added 2026-08-14). Named partial-prop bundles resolved before prop defaults; explicit props always win, mirroring Button's PresetProps/mergePresets mechanism. One preset today: "applied", the ruled applied-filter shape (size="lg" plus text-sm, chip.tsx PresetProps), which FacetBar's applied chips now ride. A preset's className merges BEFORE the caller's, so caller classes win conflicts. Presets are shorthands, never gates (AGENTS.md ground rule 8): everything a preset applies stays reachable through plain props.
  • color: ChipColor, a locally-declared 8-way union (primary | secondary | accent | neutral | info | success | warning | error). This is field-for-field identical to the shared SemanticColor union that now lives at packages/ui/src/internal/variants.ts, but Chip still declares its own copy rather than importing the shared one. The type comment (chip.types.ts:6-9) still frames this as pending a merge into a not-yet-existing shared module; that module exists now, the merge itself just has not happened. Treat ChipColor and SemanticColor as interchangeable in practice, they are not interchangeable in the type system yet.
  • variant: solid | outline | ghost | soft. solid is the unmodified default look (no extra class); the other three map to daisyUI's badge-outline/badge-ghost/badge-soft.
  • size: xs | sm | md | lg.

Selection:

  • selected / selectedColor: selectedColor overrides color entirely while selected is true.
  • onChange(isSelected, event): value-first, the state the click would produce, then the native event.
  • clickable: derives to true automatically whenever onChange or selected is set; you rarely need to pass it explicitly.

Icon:

  • Icon: IconImportProps's Icon, or literal true for the built-in check-when-selected / plus-when-not affordance (resolvedIcon, chip.tsx:69). No shipped call site sets Icon={true} as of this pass; every real icon usage passes a concrete icon key instead.
  • iconSize / iconClassName are consumed. iconProps (the third member of IconImportProps) is declared on the type but never destructured in the component (chip.tsx:42-61), so anything passed through iconProps is silently dropped. This is the same pre-existing RenderIcon passthrough gap StatusPill has.

Removable mode:

  • onRemove(event): when set, the chip's DOM shape changes to a <span> shell with a nested close <button>, because the chip body itself is no longer the clickable target and a <button> cannot nest another <button>. onRemove and onChange/selected are mutually exclusive in effect, not just in intent: when onRemove is set, the component returns the removable-span branch unconditionally (chip.tsx:85-102) and the plain clickable-button branch, the only one that reads onChange, selected, or clickable, never executes. Passing both is not a type error, but onChange will silently never fire.
  • removeLabel: what a screen reader calls the close control, defaults to Remove <label> when children is a plain string, or Remove filter otherwise.

Other:

  • skeleton, loading (drives the icon's own loading state), noBorder (drops the border, no confirmed shipped call site), tooltip / tooltipProps (wraps unconditionally, no-ops when empty, same contract as Tooltip itself and as Button's tooltip).

Sanctioned combinations#

CombinationProducesWhere usedWhy
size="sm" variant="soft" onRemove={...} in a chip block after a filter row's controlsA removable filter-value pill, one per selected valueconstructed at walmart-mvp/frontend/src/components/FacetToolbar.tsx:153-155 (FacetChips), placed at :216Canon doc 7 §A4: a multi-select trigger that can only summarize as a count hides its selection, so each value gets an individually removable chip. Placement was ruled 2026-08-12: chips gather AFTER all of the row's controls, never inline beside the one that owns them, because an inline chip pushes every later control sideways
size="sm" variant="soft" and size="sm" variant="soft" color="error", plain, no onChange/onRemoveRead-only status tags in a card toolbarspeedway/app/routes/admin/team.tsx:376-382 (plan chip, conditional "Suspended" chip), speedway/app/routes/account/accounts.tsx:444-446 (role chip)Canon doc 5 §A2: "a read-only toolbar holding a single Chip is legitimate." A plain Chip with no interaction props is a valid, common shape, not a component you must always wire up
color="warning" variant="soft" size="sm" Icon={concreteIcon} onChange={() => navigate(...)}, with a manually appended trailing icon inside childrenA chip used as a fire-once navigation trigger, not a togglespeedway/app/components/WorkflowStageChain.tsx:619-628onChange's boolean argument is deliberately ignored here (the handler always navigates); Chip has no iconRight, so the trailing arrow is placed directly in children after the label. Confirms onChange is usable as a plain click hook, not strictly a state toggle

Banned combinations#

  • onChange (or selected) together with onRemove. See the contract note above: onRemove wins unconditionally and onChange/selected are dead code on that render. If a chip needs to be both selectable and removable, that is two chips or a different component, not one Chip with both props.
  • Chip standing in for StatusPill/StatusDot or FieldChip. Canon doc 3 §A1 states this as a defect, not a preference: selection, state, and naming are three different signals and the reader needs to tell them apart at a glance.
  • Chip with selected as a two-option segmented toggle. The shipped precedent for that exact shape is a hand-rolled button pair (ToggleChip, ItemsView.tsx:62-90), not this component. If you're tempted to reach for selected to build a Yes/No or grouped/flat switch, check whether a join-pair button (canon doc 7 §B, "Admin quick toggle") fits better first.
  • Passing iconProps. It is silently dropped; if per-render icon customization beyond iconSize/iconClassName is needed, it is not currently plumbed through.

Before you adopt this#

Five questions to answer before reaching for Chip.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, Chip is inline content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? A hand-rolled two-option toggle (ToggleChip) is the shipped precedent, not selected.
  3. Does this app's kit pin reach the version this component or prop landed in? presets landed 2026-08-14; confirm the app's kit pin reaches it.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, Chip derives its own name from children or removeLabel.
  5. Which canon §A rules bind this surface, and which does the composition break? §A1 bars Chip standing in for StatusPill/StatusDot or FieldChip.

Travels with#

  • Tooltip, unconditionally, same no-op-when-empty contract as Button.
  • Dropdown/hand-rolled multi-select panels, in the removable-chip-beside-a-facet pattern: Chip renders the selected values, the facet's own control (kit Select or a hand-rolled equivalent) owns picking them.
  • RenderIcon, internally, for both the leading icon and the close-control's Close icon in removable mode.

Snippet#

<Chip key={`${f.key}:${v}`} size="sm" variant="soft" onRemove={() => f.onToggle(v)}>
{f.options.find((o) => o.value === v)?.label ?? v}
</Chip>

walmart-mvp/frontend/src/components/FacetToolbar.tsx:153-155

@versable-git/ui · reference, canon, and method, read in place