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'sPresetProps/mergePresetsmechanism. One preset today:"applied", the ruled applied-filter shape (size="lg"plustext-sm,chip.tsxPresetProps), which FacetBar's applied chips now ride. A preset'sclassNamemerges 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 sharedSemanticColorunion that now lives atpackages/ui/src/internal/variants.ts, butChipstill 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. TreatChipColorandSemanticColoras interchangeable in practice, they are not interchangeable in the type system yet.variant:solid | outline | ghost | soft.solidis the unmodified default look (no extra class); the other three map to daisyUI'sbadge-outline/badge-ghost/badge-soft.size:xs | sm | md | lg.
Selection:
selected/selectedColor:selectedColoroverridescolorentirely whileselectedis true.onChange(isSelected, event): value-first, the state the click would produce, then the native event.clickable: derives totrueautomatically wheneveronChangeorselectedis set; you rarely need to pass it explicitly.
Icon:
Icon:IconImportProps'sIcon, or literaltruefor the built-in check-when-selected / plus-when-not affordance (resolvedIcon,chip.tsx:69). No shipped call site setsIcon={true}as of this pass; every real icon usage passes a concrete icon key instead.iconSize/iconClassNameare consumed.iconProps(the third member ofIconImportProps) is declared on the type but never destructured in the component (chip.tsx:42-61), so anything passed throughiconPropsis silently dropped. This is the same pre-existingRenderIconpassthrough gapStatusPillhas.
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>.onRemoveandonChange/selectedare mutually exclusive in effect, not just in intent: whenonRemoveis set, the component returns the removable-span branch unconditionally (chip.tsx:85-102) and the plain clickable-button branch, the only one that readsonChange,selected, orclickable, never executes. Passing both is not a type error, butonChangewill silently never fire.removeLabel: what a screen reader calls the close control, defaults toRemove <label>whenchildrenis a plain string, orRemove filterotherwise.
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 asTooltipitself and asButton'stooltip).
Sanctioned combinations#
| Combination | Produces | Where used | Why |
|---|---|---|---|
size="sm" variant="soft" onRemove={...} in a chip block after a filter row's controls | A removable filter-value pill, one per selected value | constructed at walmart-mvp/frontend/src/components/FacetToolbar.tsx:153-155 (FacetChips), placed at :216 | Canon 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/onRemove | Read-only status tags in a card toolbar | speedway/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 children | A chip used as a fire-once navigation trigger, not a toggle | speedway/app/components/WorkflowStageChain.tsx:619-628 | onChange'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(orselected) together withonRemove. See the contract note above:onRemovewins unconditionally andonChange/selectedare dead code on that render. If a chip needs to be both selectable and removable, that is two chips or a different component, not oneChipwith both props.Chipstanding in forStatusPill/StatusDotorFieldChip. 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.Chipwithselectedas 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 forselectedto 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 beyondiconSize/iconClassNameis needed, it is not currently plumbed through.
Before you adopt this#
Five questions to answer before reaching for Chip.
- Does the shell, a parent layout, or a global provider already render this? Not applicable, Chip is inline content; no shell already renders it.
- Does this app already ship a local implementation of the same thing? A hand-rolled two-option toggle (
ToggleChip) is the shipped precedent, notselected. - Does this app's kit pin reach the version this component or prop landed in?
presetslanded 2026-08-14; confirm the app's kit pin reaches it. - Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable,
Chipderives its own name fromchildrenorremoveLabel. - Which canon §A rules bind this surface, and which does the composition break? §A1 bars
Chipstanding in forStatusPill/StatusDotorFieldChip.
Travels with#
Tooltip, unconditionally, same no-op-when-empty contract asButton.Dropdown/hand-rolled multi-select panels, in the removable-chip-beside-a-facet pattern:Chiprenders the selected values, the facet's own control (kitSelector a hand-rolled equivalent) owns picking them.RenderIcon, internally, for both the leading icon and the close-control'sCloseicon 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