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.
A row of chosen values the reader can take back, one cross at a time.
@pacit/components/chipspct-chips, pct-chipNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component, signal } from '@angular/core';
import { PctChip, PctChips } from '@pacit/components/chips';
/** Removal is an announcement — the application answers by shortening its own array. */
@Component({
selector: 'demo-chips',
imports: [PctChip, PctChips],
template: `
<pct-chips ariaLabel="Active filters">
@for (filter of filters(); track filter) {
<pct-chip removable (removed)="drop(filter)">{{ filter }}</pct-chip>
}
</pct-chips>
`,
})
export class ChipsDemo {
readonly filters = signal(['status: open', 'assignee: me', 'label: bug']);
drop(filter: string): void {
this.filters.update((all) => all.filter((kept) => kept !== filter));
}
}chips.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctChips, PctChip } from '@pacit/components/chips';| Exported | What it is |
|---|---|
PctChipscomponentpct-chips | A row of chosen values the user can take back: the active filters above a table, the recipients of a message. |
PctChipcomponentpct-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. |
<pct-chips ariaLabel="Filters">
<pct-chip removable (removed)="drop('Angular')">Angular</pct-chip>
</pct-chips>Each example is the component itself, not a screenshot. Open the code under any of them.
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.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
PctChipscomponent · pct-chipsA row of chosen values the user can take back: the active filters above a table, the recipients of a message.
| Name | Type | What it does |
|---|---|---|
ariaLabeldefault '' | string | The 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.defaultSize | PctSize | Size of every pill in the row; taken from the global configuration by default. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-chips | |
role | list | The 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-label | ariaLabel() || null | |
data-pct-size | size() |
PctChipcomponent · pct-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.
| Name | Type | What it does |
|---|---|---|
removabledefault false | boolean | Whether 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. |
| Name | Payload | When it fires |
|---|---|---|
removed | void | Said 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. |
The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-chip | |
role | listitem | The 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). |
Two ways in, both stable. No class names to depend on.
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.
| Part | Selector | What it is |
|---|---|---|
label | [data-pct-part="label"] | a chip's text |
remove | [data-pct-part="remove"] | the button that takes the chip back |
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.
| Token | Styles |
|---|---|
--pct-chips-item-bg{pct.surface-100}#f1f5f9 · #1e293b | Background of an item. |
--pct-chips-item-border{pct.border}#e2e8f0 · #334155 | Edge colour of an item. |
--pct-chips-item-fg{pct.text}#0f172a · #ffffff | Text colour of an item. |
--pct-chips-item-font-size{pct.font.size.md}14px | Font size of an item. |
--pct-chips-item-font-size-lg{pct.font.size.lg}16px | Font size of an item at size lg. |
--pct-chips-item-font-size-sm{pct.font.size.sm}13px | Font size of an item at size sm. |
--pct-chips-item-gap{pct.space.2}4px | Gap between a chip's label and its remove button. |
--pct-chips-item-height{pct.control.height.md}36px | Height of an item. |
--pct-chips-item-height-lg{pct.control.height.lg}44px | Height of an item at size lg. |
--pct-chips-item-height-sm{pct.control.height.sm}28px | Height of an item at size sm. |
--pct-chips-item-padding-x{pct.space.3}8px | Horizontal padding of an item. |
--pct-chips-item-padding-x-lg{pct.space.4}12px | Horizontal padding of an item at size lg. |
--pct-chips-item-padding-x-sm{pct.space.2}4px | Horizontal padding of an item at size sm. |
--pct-chips-item-radius999px | Corner radius of an item. |
--pct-chips-list-gap{pct.space.2}4px | Gap between chips. |
--pct-chips-remove-bg-hover{pct.surface}#ffffff · #0f172a | Background of the remove button on hover. |
--pct-chips-remove-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the remove button. |
--pct-chips-remove-fg-hover{pct.text}#0f172a · #ffffff | Text colour of the remove button on hover. |
--pct-chips-remove-radius999px | Corner radius of the remove button. |
--pct-chips-remove-target-min{pct.target.min}24px | Touch-target floor of the remove button. |
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;
} 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.
| key | effect | test |
|---|---|---|
Tab / Shift+Tab | moves between the remove buttons, document order | apps/sandbox-e2e/src/chips.spec.ts — the pinned chip costs nothing to step over, because there is nothing in it to stop at |
Enter / Space | presses the remove button | apps/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.
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.
libs/components/chips/src/chips.tsapps/sandbox-e2e/src/chips.spec.ts — Tab order and Enter, plus the map's one addition: where focus stands after each pressapps/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 rowapps/sandbox-e2e/src/visual.spec.ts — chips-sizes and chips-sizes-rtlforced-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.tsprefers-reduced-motion — duration from a tokennot applicable — the component has no motion at all: no transition, no loop, so the preference has nothing to slowapps/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--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 pillsdir="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-rtlNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /chips in SBX_ROUTESlibs/components/parts.snapshot.md, tools/check-parts.mjs — the Parts row abovecontrast.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.mdPCT_TEXTSlibs/components/core/src/texts.ts — chipRemove; the swap is a unit case (providePctTexts renames the cross)libs/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 unmoveddocs/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 arriveapps/sandbox/src/app/views/chips/ (the sandbox view). The published site: /components/chips — prerendered, the demo's own source is the code tablibs/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 stringsRead at build time from the tracked snapshots, not typed here.
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.
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.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.