On this page
Choices

Chips released

A row of chosen values the reader can take back, one cross at a time.

  • Import@pacit/components/chips
  • Selectorspct-chips, pct-chip

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

status: openassignee: melabel: bug

chips.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctChips, PctChip } from '@pacit/components/chips';
ExportedWhat it is
PctChipscomponentpct-chipsA row of chosen values the user can take back: the active filters above a table, the recipients of a message.
PctChipcomponentpct-chipOne chosen value in a pct-chips row. Its content is the application's — projected, never generated — and the one thing the chip may add to it is the control that takes the value back.
<pct-chips ariaLabel="Filters">
  <pct-chip removable (removed)="drop('Angular')">Angular</pct-chip>
</pct-chips>
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

Sizes, and chips that stay

size rides the shared axis. A chip without removable is a plain word — a tag the reader cannot take back — and never draws the button it would not act on.

AngularSignalsTokensOpenAssigned to meThis week
API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

PctChipscomponent · pct-chips

A row of chosen values the user can take back: the active filters above a table, the recipients of a message.

Inputs

NameTypeWhat it does
ariaLabeldefault ''stringThe accessible name of the list. Optional, and deliberately without a text key behind it: a list is allowed to be nameless, and a library default would have to guess what the list holds — "Chips" names the paint, not the content (0051). Two rows on one page ("recipients", "active filters") are told apart here.
sizedefault config.defaultSizePctSizeSize of every pill in the row; taken from the global configuration by default.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-chips
rolelistThe role is static, and that is measured rather than assumed: a role="list" with zero items raises nothing in any engine's audit (0051 quotes the probe), so an empty row needs no machinery — unlike the listbox, whose empty panel is a critical violation and taught the select to keep its role conditional.
aria-labelariaLabel() || null
data-pct-sizesize()

PctChipcomponent · pct-chip

One chosen value in a pct-chips row. Its content is the application's — projected, never generated — and the one thing the chip may add to it is the control that takes the value back.

Inputs

NameTypeWhat it does
removabledefault falsebooleanWhether the chip draws the control that takes it back — off by default. A mixed row is ordinary: the one filter the view cannot stand without keeps its removable off and shows no button, and the focus repair steps over it.

Outputs

NamePayloadWhen it fires
removedvoidSaid when the user presses remove. The collection is the application's, so this is an announcement and not an act: the consumer shortens their own array and the row follows on the next render — or does not, and the chip is correct to stand still.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-chip
rolelistitemThe platform's own name for "one of these" — pct-tab's move with tabpanel: the native <li> is out of reach on a custom element, so the role is declared on the element that is actually there (req-a11y-built-in).
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctChipsHarness, PctChipHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
label[data-pct-part="label"]a chip's text
remove[data-pct-part="remove"]the button that takes the chip back

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-chips-item-bg{pct.surface-100}#f1f5f9 · #1e293bBackground of an item.
--pct-chips-item-border{pct.border}#e2e8f0 · #334155Edge colour of an item.
--pct-chips-item-fg{pct.text}#0f172a · #ffffffText colour of an item.
--pct-chips-item-font-size{pct.font.size.md}14pxFont size of an item.
--pct-chips-item-font-size-lg{pct.font.size.lg}16pxFont size of an item at size lg.
--pct-chips-item-font-size-sm{pct.font.size.sm}13pxFont size of an item at size sm.
--pct-chips-item-gap{pct.space.2}4pxGap between a chip's label and its remove button.
--pct-chips-item-height{pct.control.height.md}36pxHeight of an item.
--pct-chips-item-height-lg{pct.control.height.lg}44pxHeight of an item at size lg.
--pct-chips-item-height-sm{pct.control.height.sm}28pxHeight of an item at size sm.
--pct-chips-item-padding-x{pct.space.3}8pxHorizontal padding of an item.
--pct-chips-item-padding-x-lg{pct.space.4}12pxHorizontal padding of an item at size lg.
--pct-chips-item-padding-x-sm{pct.space.2}4pxHorizontal padding of an item at size sm.
--pct-chips-item-radius999pxCorner radius of an item.
--pct-chips-list-gap{pct.space.2}4pxGap between chips.
--pct-chips-remove-bg-hover{pct.surface}#ffffff · #0f172aBackground of the remove button on hover.
--pct-chips-remove-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the remove button.
--pct-chips-remove-fg-hover{pct.text}#0f172a · #ffffffText colour of the remove button on hover.
--pct-chips-remove-radius999pxCorner radius of the remove button.
--pct-chips-remove-target-min{pct.target.min}24pxTouch-target floor of the remove button.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-chips-item-bg: #ccfbf1;
  --pct-chips-item-fg: #134e4a;
  --pct-chips-item-border: #99f6e4;
}
status: openassignee: melabel: bug
Accessibility

Measured, not assumed

Pattern: none — there is no APG pattern for chips, and this component does not invent one (0051). What there is: list / listitem for the count a screen reader owes its user first, and a real <button> for every removal. Named in the class JSDoc.

Keyboard

keyeffecttest
Tab / Shift+Tabmoves between the remove buttons, document orderapps/sandbox-e2e/src/chips.spec.ts — the pinned chip costs nothing to step over, because there is nothing in it to stop at
Enter / Spacepresses the remove buttonapps/sandbox-e2e/src/chips.spec.ts — and focus lands on the next cross, in three engines

Both rows are the platform's: the component ships zero key handlers — no Delete, no arrows, no roving tabindex (req-api-platform). The serial-removal behaviour Material builds a grid pattern for is delivered here by the repair alone.

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/chips/src/chips.ts
MeasuredKeyboard map tested key by keyapps/sandbox-e2e/src/chips.spec.ts — Tab order and Enter, plus the map's one addition: where focus stands after each press
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /chips in SBX_ROUTES, so the WCAG 2.2 AA sweep takes it. An empty role="list" was probed against the audit in three engines before the role was made static — zero violations, which is why there is no machinery for the empty row
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — chips-sizes and chips-sizes-rtl
Measuredforced-colors: active — no state carried by colour alonelibs/components/chips/src/chip.scss — the pill's inside is one step of the ramp and disappears with the mode; the boundary rides on the border (CanvasText), the cross takes ButtonText, hover answers in Highlight/HighlightText. The reading: apps/sandbox-e2e/src/forced-colors.spec.ts
n/aprefers-reduced-motion — duration from a tokennot applicable — the component has no motion at all: no transition, no loop, so the preference has nothing to slow
MeasuredTouch target ≥ 24×24 px outrightapps/sandbox-e2e/src/chips.spec.ts — the cross clears 24×24 at every size of the row: --pct-chips-remove-target-min is {pct.target.min} outright, so at sm the 24 px button stands inside a 28 px pill and the pill, not the button, is what looks small
MeasuredSize axis aligned to --pct-control-height-*libs/tokens/src/component.chips.json — item-height IS the control axis, so a row of chips lines up with a field of the same size by definition; apps/sandbox-e2e/src/chips.spec.ts reads 28/36/44 off the pills
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a dir="rtl" screenshotlibs/components/chips/src/chip.scss — flex order and a logical gap place both the row and the cross, with not one physical property. The geometry: apps/sandbox-e2e/src/rtl.spec.ts (first chip on the right, every cross on its label's left); the screenshot: chips-sizes-rtl
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /chips in SBX_ROUTES
n/aFormsnot applicable — the row holds no value a form owns: the collection lives in the application, and the input-chips variant (values typed into a combobox) is the select family's road on the day it is walked
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs — the Parts row above
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — four entries: the label (AA error), the cross at rest and under hover (UI error — a functional drawing owes the non-text floor), the pill's edge (UI warn, with the reason written beside it); libs/tokens/tokens.snapshot.md
MeasuredStrings through PCT_TEXTSlibs/components/core/src/texts.ts — chipRemove; the swap is a unit case (providePctTexts renames the cross)
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./chips row. Measured the day it landed, 2026-09-01: the one new string cost ./core +20 B, fanned out to every entrypoint that carries it, and ./skeleton, which carries no ./core, stood unmoved
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox, and it is the reading that changed the component. The template argued the cross needed texts().chipRemove alone, the label beside it being heard anyway; all three said Remove at every one of five chips and named none of them (lesson-218). The name is composed now, and re-read on the same three: Remove In stock · button. (Orca), Remove Under 50, button (NVDA), Remove Free shipping button (VoiceOver) — ten crosses on this view, each naming its own value. The named list and the item count arrive in all three. The FIRST cross is silent for NVDA and VoiceOver, and that is the instrument rather than the component: enter puts focus there outright, and neither reader announces a focus it did not see arrive
MeasuredA docs page with live examplesapps/sandbox/src/app/views/chips/ (the sandbox view). The published site: /components/chips — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/chips/src/chips.spec.ts — 25 cases; libs/components/mutation.snapshot.md — chips.ts at 90.74, 49 killed with zero clock-kills, and all five survivors named: isDevMode() forced true (the fourth component in a row), reversing the whole order instead of the predecessors (unreachable difference — the prefix of the walk is dead wherever it decides), above?. on a parent no document can null, and the two token labels, which are debug strings
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

89.47%mutation score51 of 57 mutants killed
12 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
4colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
22elements in the preview6 listeners · 1 render to settle · 4971 µs in jsdom, held exactly but for the clock

Why it is built this way

The whole component is one addition to the platform: where focus goes when the button under it disappears. Removing the focused control drops activeElement on <body> in all three engines — measured before anything was written — so the row repairs it: the next chip's cross, the previous ones as a fallback, and Enter, Enter, Enter empties the row with no Tab between them.

Known limitations

  • No focus placement when the row empties. The last removal has no surviving cross to land on, and the component cannot know what the page wants focused then — the heading above the list, the input beside it. The consumer who removed the last value is the one who knows what stands next to the row.
  • The repair stands for one render. A removal the consumer performs seconds later, behind a confirmation, moves no focus: the user has navigated away, and yanking focus back would be the repair causing the defect it exists to prevent.
  • A wrapped chip is outside the repair's map. The row holds only its own children — a chip inside an extra element is a listitem whose list is not directly above it, which is broken ARIA before it is a repair problem. A dev-mode warning names it at first render.
  • No Delete, no arrows, no roving tabindex. There is no APG pattern for chips, and the library does not invent keyboard grammars; Tab and Enter reach everything, and the repair makes serial removal cheap.
  • No input-chips. Values typed into a combobox and kept as tokens are the select family's road; this component renders values chosen elsewhere.