Filter Chips
Build a filter query from editable chips.
import { FilterChips } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use filter chips where filters also read as text: a log search, an order list, an issue tracker. Each filter is a chip like priority:in(p1,p2), people who think in queries can type them, and the chips can mirror controls elsewhere on the page. For free-form values that aren't a query (topics, email domains), use a Tag Input.
import { Field, FieldLabel, FilterChips } from '@youj/react'
export function IssueSearch() {
const [query, setQuery] = useState(['priority:in(p1,p2)'])
return (
<Field>
<FieldLabel>Issues</FieldLabel>
<FilterChips
value={query}
onValueChange={setQuery}
validate={(token) => (/^[a-z]+:\S/.test(token) ? undefined : 'Write a filter as field:value.')}
/>
</Field>
)
}A token ends at a space, never inside brackets or quotes, so priority:in(p1, p2) stays whole. A finished field:value becomes a chip as you type the space after it, and Enter adds whatever is typed. A pasted query adds each of its tokens. Click a chip, or focus it and press Enter, and its text returns to the field to edit.
validate gets the new token and the current list, and returns a reason to refuse it. A refused token stays in the field: the frame shakes, the border turns destructive, and the reason grows in under it and is read out. A duplicate isn't added; the chip that already holds it flashes.
Description
An Issues field holding the chips label:bug and priority:in(p1,p2), with a hint to type assignee:me and a space.
Chips are keyed by their field (the text before the colon), so when a control changes a filter, its chip rewrites itself in place instead of leaving and coming back. Derive value from your filters, and parse the tokens back in onValueChange: the Filter panel block's order search does both.
Code
filter-chips-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsvalue, defaultValue, onValueChange… (13)
Props
| Prop | Type | |
|---|---|---|
value | string[] | – |
defaultValue | string[]default [] | [] |
onValueChange | (value: string[]) => void | – |
validate | (token: string, tokens: string[]) => string | null | undefined | voidReturns a reason to reject a token, or nothing to accept it. | – |
placeholder | stringDefault: "Add a filter…" (the | – |
disabled | boolean | – |
name | stringAlso submits each token with a native form, under this name. | – |
id | string | – |
ref | Ref<HTMLInputElement> | – |
onBlur | () => void | – |
className | string | – |
aria-label | string | – |
aria-labelledby | string | – |
MotionChip enters, chip leaves, chips reflow… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Chip entersfades into its slot while the field makes room (PresenceList) | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Chip leavesfades where it stands, then the rest close up | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) | |
| Chips reflowglide to their new places | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Refusedthe frame shakes a decaying 4, 3 and 2px | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Reasongrows in under the field (Collapse) | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Duplicate flashthe chip that holds it tints at once, then fades back | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
No shake and no glide: chips appear and leave in one frame. The duplicate flash and the reason still show, because they carry the message.
Reduced motion
A refused token doesn't shake and chips don't glide. The flash and the reason still appear.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The field is named by its
FieldLabel,aria-labeloraria-labelledby, and the chips are a list named "Active filters". A refused token setsaria-invalidand is described by its reason, arole="alert"message. - In the field, Backspace when it's empty, or the left arrow at its start, reaches the last chip. On a chip, the arrows move between chips, Home and End jump to the ends, Enter or F2 edits it, Backspace or Delete removes it and focus lands on its neighbor, and Escape returns to the field. Each chip describes these keys.
- Each chip's remove button is named after its filter. Additions, removals and duplicates are announced politely.
- With
name, each token is submitted with a native form under that name.