Appearance
Chip
A pill that is on or off. Reach for it when a row of options narrows what a list below shows: the genres above a catalogue, the languages of a feed, the switches along a toolbar. Each chip names one option and a press turns it on or off; the chips that are on say what the list is showing. When the options are exclusive and the row is the whole control, Tabs says so more clearly; when the option takes effect on a form's submit rather than at once, a Checkbox does.
A chip is a toggle button and announces itself as one: the role is button and pressed follows selected, which is what aria-pressed is for. A chip that is never on, such as "Clear filters" or "Back", is the same component with selected left off, and it reads as a plain button.
tsx
/** The genres, with how many tracks each has, as a catalogue would give them. */
const GENRES = [
{ name: 'Electronic', count: '366,280' },
{ name: 'Metal', count: '119,205' },
{ name: 'Folk', count: '32,476' }
] as const;
/** A heart, on the 24 grid every icon here is drawn on. */
const HEART =
'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z';
/**
* Two rows of chips, and the sentence they add up to.
*
* The first row picks one genre or none. Each chip is controlled: the
* application holds `genre`, every chip reads whether it is the one,
* and a press writes the cell rather than the chip. "All" is the chip
* that is on until another is pressed, so the row always says what it
* is showing, and its `name` says so to a screen reader too: "All,
* showing", then "Show all" once a genre is chosen. The others carry
* a `count`, which is drawn after the word and read after it.
*
* The second row is outlined and small, the shape for a toolbar. Two
* chips are switches that hold their own value in a cell each; "Liked"
* has a glyph before its word; "Clear filters" is never on, so it reads
* as a plain button; and "Offline only" is disabled, with a
* `description` saying why.
*
* Nothing here names a colour. The sheet, the inversion when a chip is
* on, the ring, the wash, the hover and the press all come from the
* control tokens of whatever theme the tree inherits.
*/
export function Filters(_inputs: Inputs<{}>, _ctx: ComponentContext) {
const genre = internalState<string | null>(null);
const verified = internalState(false);
const liked = internalState(true);
const summary = computed(() => {
const parts = [genre.value ?? 'every genre'];
if (verified.value) {
parts.push('verified artists only');
}
if (liked.value) {
parts.push('liked tracks');
}
return `Showing ${parts.join(', ')}.`;
});
const clear = (): void => {
genre.value = null;
verified.value = false;
liked.value = false;
};
return (
<column gap={16} padding={20} width={percent(100)} height={percent(100)} y="center">
<row gap={8} rowGap={8} y="center" flexWrap="wrap" role="group" label="Filter by genre">
<Chip
label="All"
name={computed(() => (genre.value === null ? 'All, showing' : 'Show all'))}
selected={computed(() => genre.value === null)}
onPress={() => (genre.value = null)}
/>
{GENRES.map(entry => (
<Chip
key={entry.name}
label={entry.name}
count={entry.count}
selected={computed(() => genre.value === entry.name)}
onPress={() => (genre.value = genre.value === entry.name ? null : entry.name)}
/>
))}
</row>
<row gap={6} rowGap={6} y="center" flexWrap="wrap">
<Chip
label="Verified artists only"
variant="outlined"
size="small"
selected={verified}
onPress={next => (verified.value = next)}
/>
<Chip
label="Liked"
icon={HEART}
variant="outlined"
size="small"
selected={liked}
onPress={next => (liked.value = next)}
/>
<Chip label="Clear filters" variant="outlined" size="small" onPress={clear} />
<Chip
label="Offline only"
description="Save a track to filter by it"
variant="outlined"
size="small"
disabled
/>
</row>
<text text={summary} textStyle="bodySmall" color="textMuted" />
</column>
);
}Tab into the rows and try them. The genre row is controlled: the application holds one cell, every chip reads whether it is the chosen one, and a press writes the cell rather than the chip. "All" is on until a genre is pressed, and its accessible name says so: "All, showing", then "Show all". The toolbar row below is outlined and small, with two switches, a glyph, a chip that is never on and one that is disabled.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
label | string | '' | The word on it, and the accessible name unless name says more |
name | string | the label | The accessible name, when it should say more than the word; the count joins the label if not |
description | string | '' | Longer help, for a word that cannot say everything |
selected | boolean | none | Whether it is on, when the application owns it. Supplying this makes the chip controlled. |
defaultSelected | boolean | none | The starting value, when the chip owns it. Supplying this makes the chip self-managing. |
onPress | (selected: boolean) => void | none | Fired on a click and on Enter or Space, with the value the chip would take |
count | number or string | none | A figure after the word, in the same colour at the normal weight, read after the word too |
icon | string | none | SVG path data for a glyph before the word, on the usual 24 grid |
variant | ChipVariant | 'filled' | What the chip is made of: filled or outlined |
size | ChipSize | 'medium' | How big it is: small or medium |
textStyle | UiTypographyRole | the size's | The role the word is set in, for a code or a figure that wants the monospace role |
disabled | boolean | false | Refuses presses and keys, and draws the word in the disabled foreground token |
ref | UiNodeRef | none | Receives the node that is the chip, for focusing it or anchoring something to it |
Neither selected nor defaultSelected has a default in the sense of a value the component substitutes. Supplying neither makes a plain button: never on, with onPress firing true, the value a chip that could be on would take. That is deliberately not what a checkbox does with neither, because a chip that is never on is a thing a row of chips actually has, and a "Clear filters" that quietly turned itself on when pressed would be a bug. Supplying both throws.
Every prop takes a plain value or an Observable of one, and the layout props on the library page apply here too.
Controlled and uncontrolled
tsx
// Controlled: the application owns the value, and the chip shows it.
<Chip label="Metal" selected={computed(() => genre.value === 'Metal')} onPress={() => (genre.value = 'Metal')} />
// Uncontrolled: the chip owns the value, and reports changes if asked.
<Chip label="Verified only" defaultSelected onPress={next => save(next)} />The controlled form is the one a row of filters wants, because one cell holds the choice and every chip reads it; there is no way for two chips to be on at once, and nothing to keep in step. It also lets the application decline: onPress fires with the value the chip would take, and if nothing writes back then the chip does not move.
One word, and a name that can say more
label is the word on the chip and, by default, what a screen reader says. A chip in a row that filters a page reads "Metal"; what pressing it does is "Show Metal, 119,205 tracks", and that is worth saying to someone who cannot see the row. name replaces the announced name without touching the word:
tsx
<Chip label="Metal" name={computed(() => (on.value ? 'Metal, showing' : 'Show Metal, 119,205 tracks'))} />count is for a figure that should be seen as well as heard. It is drawn after the word at the normal weight, so it reads as a count beside a word rather than as a second word, and it joins the accessible name as "Metal, 119,205" unless name replaces it.
The two axes
| Axis | Values | What it says |
|---|---|---|
variant | filled, outlined | What the chip is made of |
size | small, medium | How big it is |
filled is a sheet in controlBackground with muted words; on, it inverts, painting itself in controlForeground with the sheet's colour for words, which is ink on chalk becoming chalk on ink in one appearance and the reverse in the other. It is the loud choice for a row that filters a whole page. outlined is a ring of controlBorder around muted words; on, it fills with the selection pair and takes the accent for its ring, which is quieter and suits a toolbar where a dozen chips sit beside the words they filter.
The words of a chip that is off are textMuted rather than controlForeground, on purpose: a row of filters is a row of quiet things with one or two loud ones, and that contrast is what tells a reader which is which.
small and medium share their vertical metrics with Button's small and medium, so a chip and a button on one row are the same height, and take one step more of horizontal padding because a pill wants more room at its round ends than a rectangle does. Both are read once when the chip is built, as the button's axes are: a chip that has to change variant should change its key.
Hover, press and focus
Built in, and not a prop.
The hovered ground of a chip depends on whether it is on, and the interactive modifier's overrides are ordinary values written over the bound one. The private chips that came before this component learnt what that does: a chosen chip under the pointer went light while its words stayed white, so the one chip the person was pointing at was the one they could not read. This chip hands the modifier cells rather than tokens, which the modifier host follows for as long as it is attached, so the hovered ground moves with the state. Off, either variant hovers to controlBackgroundHovered and presses to controlBackgroundPressed, as every other control in the library does. An outlined chip that is on does the same over its wash. A filled chip that is on is painted in the foreground itself and has nowhere to move to, so it dims, to 0.88 and then 0.76, as the filled button does.
The cursor is pointer and the focus ring is CONTROL_FOCUS_RING, the same one the inputs tier draws. Neither is optional.
Keyboard
The chip is one tab stop. Both bindings toggle, and the element consumes them, so nothing above the chip sees the key.
| Key | What it does |
|---|---|
Space | Toggles the chip |
Enter | Toggles the chip |
Tab | Not bound: focus moves on as it normally would |
A disabled chip takes neither.
Semantics
| What | Value |
|---|---|
| Role | button, on the pill that takes focus |
| Name | name, else label with count after it |
| Description | description |
| States | pressed while selected |
| Disabled | disabled is carried on the record, and inherited by everything under it |
The states arrive as they change rather than being read once, so a chip the application turns on from elsewhere updates what an assistive technology hears without anything re-rendering. The word inside the pill has no record of its own, for the reason a checkbox's label has none.
Colours
None of them are props. Chip reads the control tokens from whatever theme it inherits, like everything else in the library, and restyling one is a theme provider around it. That is the mechanism themes and the environment describes.
What this page was checked against
Chip.spec.ts mounts the component with gesso-testing and asserts the label as the word and the name, pressed following selected, that a controlled chip reports a press and does not move until the application writes back, that defaultSelected makes it self-managing and supplying both throws, that Space and Enter toggle it and a disabled one refuses both, that name and count shape the announced name, that every colour on both variants in both states is a palette name, that the hover token and the dimmed opacity follow the state while the pointer stays, and that the sizes name a type role rather than a size. ChipExample.spec.ts asserts what the example above claims, by role and name. The keyboard gallery reaches the chip by Tab and turns it on with Space.
Next
Tabs is the control for options that are exclusive and are the whole of a screen's navigation, and Checkbox is for an option that takes effect when a form is submitted.