Filter by allergen
Menu options
Showing menu items matching 2 selected.
Overview
A chip is one short label inside a pill — a payment method, a dietary restriction, an applied filter. Chips show up in groups, carry a single piece of metadata each, and let the user touch that metadata: see it, remove it, toggle it. We use chip across the system because tag already means a free-text marker the user types into a CMS — the words look the same, the components are not.
Chip vs Badge vs Status pill
A chip is something the user touches — removes, selects, drags. A badge is a non-interactive count (12 unread, 3 disputes). A status pill is a non-interactive state marker (Live, Draft, Archived). All three are pills; only the chip is a control.
Anatomy
Three slots. Optional leading icon, label, optional trailing remove button. Pill shape — radius 9999 px — never a rounded rectangle.
Leading icon (optional)
16 px Material outlined glyph in the chip's text colour. Skip when the label alone is obvious — never use the icon for decoration.
Label
Inter Tight Medium 14 / 20 in
text.primary(off) ortext.inverse(on, for selectable mode). Sentence case. One short phrase.Remove button (removable mode)
20 × 20 hit target with a 14 px close glyph. Has its own
aria-labelso screen readers announce the action separately from the chip itself.Surface
background.accent.overlay.light.subtler(rgba(0,0,0,0.04)) when off; flips tobackground.selected(#0f0f0f) when a selectable chip is on. The selected state inverts the label totext.inverse.
Modes
Three modes, one primitive. The visual delta is small; the semantic delta — interactive vs read-only — is the whole point.
Default
A static label. Use as a read-only marker — payment method, region, card scheme. Not interactive.
Removable
A user-applied marker that the user can take off. Use for tag editors, recipient lists, applied filters.
Selectable
A toggle inside a multi-select group. The on state flips to the inverse surface — pure black + white text.
Sizes
Two sizes. md is the default at 36 px tall; smdrops to 28 px for dense surfaces — table cells, packed metadata rows, places where the row height already dictates the chip's size.
md — default
36 px tall. The default for filter rails, applied-tag lists, and settings groups.
sm — dense
28 px tall. Use inside table cells, list rows, and packed metadata slots where the row height already constrains everything.
States
The selectable mode carries the system's only chromatic state for chips: off (overlay-light) and on (pure-black surface, white label, leading check glyph). Hover lifts the off surface slightly; focus shows a 2 px ring in border.selected.
Leading icon
A 16 px Material outlined glyph anchors a chip when the row carries icon-and-label rhythm — order categories, channel filters, item types. The icon never replaces the label; chips are read by word first, glyph second.
Leading icon
A 16 px Material outlined glyph in front of the label. Use when the row is dense enough that an icon helps scanning — order categories, item types, status filters. Skip the icon when the label alone is obvious.
Applied filters
The most common composition in the product: a row of removable chips that name the user's currently-applied filters, with a “Clear all” affordance to drop them in one move. Each chip removes itself; the surrounding rail rewraps.
Applied filters
Behavior
- Chips travel in groups. A single chip on a page is almost always a sign you wanted a Badge or a Status pill. The chip earns its place when there are two or more — a filter rail, a tag list, a multi-select.
- Wrap, don't scroll. The group rail wraps onto multiple lines with an 8 px gap. Horizontal scrolling for chips is forbidden — the user can't see what they've applied without dragging.
- Removal is reversible. When a chip's removal is destructive (drops a setting, cancels a recipient), pair the rail with an undo affordance — either a Toast on remove or a Clear-all-then-undo pattern. Don't make the user re-find what they had.
- Selectable chips are multi-select by default. Use a Choice list (radio cards) when the answer is exactly one. Use a Segmented control when the choice is one-of-three. Reach for selectable chips when the user can hold zero or many at once.
- Truncation is forbidden. Chips never
text-overflow: ellipsis. If a label doesn't fit, the label is too long for the component — rename it or promote the row to a List item.
Accessibility
- Roles: selectable chips render as
role="button"witharia-pressedtracking the on / off state. Default chips have no role — they're plain text. Removable chips delegate the interactive role to the inner remove button so the chip itself stays read-only and the close affordance is the focus target. - Keyboard:
Space/Entertoggle a selectable chip;Tabmoves between chips inside a group, then onto the remove button if present.Backspace/Deleteon a focused removable chip is wired through toonRemove. - Group label: wrap chips in a
ChipGroupwith anaria-labelthat names what the group represents (“Applied filters”, “Allergens”). Screen readers announce the label once and step through each chip in DOM order. - Remove labels: the close button gets its own
aria-label— “Remove Vegan”, not just “Remove” — so voice users can target it by name. - Contrast: the off surface gives
text.primarya 17 : 1 contrast on white. The on surface flips to inverse —#0f0f0fwithtext.inverseat 21 : 1. - Touch targets: the chip itself is 36 px tall (28 px on
size="sm"); the remove button is 20 × 20 px, but the entire chip can also be focused and dismissed withBackspace, so reaching for the small close glyph is never the only path.
Code
The component composes from a single Chip primitive plus a ChipGroup wrapper for layout + group labelling.
tsx
import { Chip, ChipGroup } from "@flatpay-dk/ui";
import RestaurantIcon from "@mui/icons-material/RestaurantOutlined";
// Default — static label
<Chip label="Visa" />
// Removable — applied filters / tag editor
<ChipGroup ariaLabel="Applied filters">
{filters.map((f) => (
<Chip
key={f.id}
label={f.label}
mode="removable"
onRemove={() => dropFilter(f.id)}
/>
))}
</ChipGroup>
// Selectable — multi-select rail
const [active, setActive] = useState<Set<string>>(new Set());
<ChipGroup ariaLabel="Filter by allergen">
{ALLERGENS.map((a) => (
<Chip
key={a.id}
label={a.label}
mode="selectable"
selected={active.has(a.id)}
onSelectChange={(next) => {
setActive((prev) => {
const out = new Set(prev);
next ? out.add(a.id) : out.delete(a.id);
return out;
});
}}
/>
))}
</ChipGroup>
// Leading icon — when the row carries icon-and-label rhythm
<Chip label="Food" leadingIcon={RestaurantIcon} />
// Dense — sm size for table cells / packed metadata
<Chip size="sm" label="Vegan" mode="selectable" selected />Best practices
Chips fail loudly when they're used as a substitute for a Badge, a Button, or a List item. The questions below catch the most common mistakes.
Do
Use a removable chip for filters the user has applied. The X is the affordance — the user can drop the filter without leaving the page.
Don't
Don't use a chip to display a count. That's a Badge. The chip looks interactive but isn't, so the user keeps trying to click.
Do
Use selectable chips for multi-select rails — the user can hold zero, one, or many at once.
Don't
Don't use selectable chips for a one-of-three answer. That's a Segmented control — the visual difference signals the difference in cardinality.
Do
Wrap chips onto multiple lines with an 8 px gap. The user sees every applied filter at a glance.
Don't
Don't horizontally scroll a chip rail. The user can't see what they have without dragging — and the scrollbar steals focus from the chips themselves.
Props
Chip
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | — | The chip's text. Inter Tight Medium 14/20. Sentence case, one short phrase. |
| mode | "default" | "removable" | "selectable" | "default" | Interaction mode. Default is read-only; removable adds a trailing close button; selectable turns the chip into a multi-select toggle. |
| size | "sm" | "md" | "md" | 36 px tall (md) or 28 px tall (sm). Use sm only inside dense surfaces — table cells, packed list rows. |
| selected | boolean | false | Selectable mode only. Controlled on/off — pair with onSelectChange. |
| onSelectChange | (selected: boolean) => void | — | Selectable mode only. Fires on click and on Space / Enter. |
| onRemove | () => void | — | Removable mode only. Fires when the trailing close button is activated. |
| leadingIcon | ComponentType | — | Optional 16 px Material outlined glyph component. Use only when the label benefits from a glyph. |
| disabled | boolean | false | Locks the chip and dims the label. Selectable chips can't be toggled; removable chips can't be dismissed. |
| removeLabel | string | — | Override the close button's aria-label. Defaults to 'Remove {label}'. |
ChipGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| ariaLabel | string | — | Accessible name for the group — names what the chips represent. Required for screen readers. |
| children | ReactNode | — | Chip elements. The group wraps onto multiple lines with an 8 px gap. |