HabChip
The interactive sibling of HabBadge: selectable, removable, or a filter.
<HabChip tone="success">Online</HabChip>Use HabChip when the user can do something with it. Use HabBadge when it is a label they cannot act on. The two look deliberately similar, so the choice is about behaviour, not appearance.
Shape carries that rule by itself: a pill can be touched, a rounded rect cannot. A chip is always a pill and there is no shape prop. There used to be a label-shaped variant, and it was indistinguishable from a Badge — identical radius, fill, border and weight, differing by 1px of type. What those were actually for, a static tag in an arbitrary color, is now HabBadge's color.
Variants
contained is a tinted surface, plain an outlined and faded one — reach for it for "not granted" — and solid a full-color fill.
The 1px edge on contained is load-bearing. --hab-contained sits about 2% off --hab-bg on a light tinted page, so fill alone makes the chip disappear. The edge is mixed from the tone at 20%, and at 40% for plain.
contained and plain print the tone's -text leg, never its base color. A contained chip printing the base on --hab-contained measured 3.38:1 ("Online") and 2.86:1 ("Degraded") at 12px — both below AA.
solid is for the semantic tones only. Each has a measured on-fill -fg leg; an arbitrary color has no such partner, so solid + color renders contained instead and warns. Borrowing --hab-accent-fg would render ink-on-dark under one product and white-on-light under another.
An arbitrary color
<HabChip :color="role.color">{{ role.label }}</HabChip>A tone and an arbitrary color take exactly the same code path — the only difference is what --hab-chip-c holds. The text is mixed toward --hab-fg at --hab-tint-text-mix (45%), the highest ratio that clears 4.5:1 for every hue. Pass the base color, not a pre-darkened one.
Filters
Attach a @click and the chip becomes a real control: role="button", a tab stop, a pointer cursor, aria-pressed, and Enter/Space.
<HabChip :selected="active.includes(facet)" @click="toggle(facet)">
{{ facet }}
</HabChip>selected overrides the tone entirely: a selected chip reads as the accent regardless of what it is a chip of. It deliberately keeps its tint on hover — two tinted states in one row would read as two selections.
selected requires a click handler. A chip that paints the selected treatment but cannot be clicked is a filter the user cannot clear, and aria-pressed is invalid on a plain <span>, so the state would be visible to the eye and unannounced to a screen reader. Without a handler it is ignored, and Habitus warns.
Removable
<HabChip @remove="tags = tags.filter((t) => t !== tag)">{{ tag }}</HabChip>The × is a real <button> with its own accessible name, and it stops propagation so it works inside a clickable chip. That name comes from remove-label, which has no default: Habitus holds no UI text of its own, so the string is yours to supply in your application's language. A removable chip without one warns.
The chip shell is a <span role="button"> and never a real <button>, precisely because of that nesting: <button> inside <button> is invalid HTML, the browser may reparent it, and the inner click target stops being reliable.
A clickable chip that is also removable nests two controls
That is inherent to the pattern, not a Habitus choice — but be aware that a role="button" containing a <button> is nested interactive content, which some screen readers navigate poorly. If a chip needs both, consider whether the row around it should own the click instead.
Icon only
A square at the control height, still pill-shaped. Pass an aria-label if it is clickable — the icon is decorative, so without one the chip has no accessible name, and Habitus warns.
Code values
mono sets the label in the mono face, for a value that is code rather than a word — an id, a type. It replaces the sans rather than adding to it; see HabButton.
Long labels
A chip is never wider than its container: a label that does not fit ends in an ellipsis. The chip does not name itself in full on hover — whether the cut-off part matters is the consumer's call. When it does, as with a generated id, wrap the chip in a HabTooltip carrying the whole value; the tooltip takes the chip as its trigger, so no extra element changes the row's layout.
<HabTooltip :label="id">
<HabChip mono @click="pick(id)">{{ id }}</HabChip>
</HabTooltip>Accessibility
- Inert by default: no role, no tab stop. It becomes a control only when a click handler is attached, and then it always gets the full keyboard contract.
aria-pressedis present whenever the chip is clickable, so a clickable chip is always presented as selectable. If yours is not a toggle, that is a sign the interaction belongs on the row rather than the chip.- Enter and Space dispatch a genuine click, so your modifiers and
.stopbehave as they would from a mouse. - The native
:focus-visibleoutline is the focus indicator, on the chip and on the × alike. - Never let the tone or color be the only thing carrying meaning. The label has to say it too.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color | string | undefined | An explicit color, overriding tone. Any CSS color, var() included. The text is mixed toward --hab-fg so it stays readable — pass the base color, not a pre-darkened one. variant="solid" is ignored with an arbitrary color, because there is no measured on-fill partner for it, and renders contained instead. |
icon | string | undefined | Leading icon as SVG path data, e.g. mdiTag from @mdi/js. |
iconOnly | boolean | false | Icon with no label — a square at the control height. |
mono | boolean | false | Sets the label in the mono face: for a value that is code — a type, an id, an environment variable's name — not a word. One font, never both: a mono class added from outside would compete with the component's own sans by stylesheet order. |
removeLabel | string | undefined | Accessible name for the trailing dismiss button. Required on any chip with an @remove handler: Habitus ships no default, so the string is the host application's to translate. A Habitus addition — the design contract hardcodes the English string. |
selected | boolean | false | Selected filter state: accent tint, accent border, aria-pressed. Requires a click handler. A selected chip with no handler is a filter the user cannot clear, and aria-pressed is invalid on a plain <span>, so the state would be visible to the eye and unannounced to a screen reader. Ignored without one. |
tone | "error" | "neutral" | "accent" | "info" | "success" | "warning" | "neutral" | Semantic color role. |
variant | "solid" | "contained" | "plain" | "contained" | Surface treatment. |
Slots
| Slot | Bindings | Description |
|---|---|---|
default | any | The label. Omitted when iconOnly is set. |
leading | any | Leading adornment, replacing the glyph. Takes precedence over icon. |