Skip to content

Filter Chips

Build a filter query from editable chips.

import { FilterChips } from '@youj/react'

Installation & usage

npm i @youj/react

For 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.

Reference

Propsvalue, defaultValue, onValueChange… (13)

Props

FilterChips props
PropType
valuestring[]–
defaultValuestring[]

default []

[]
onValueChange(value: string[]) => void–
validate(token: string, tokens: string[]) => string | null | undefined | void

Returns a reason to reject a token, or nothing to accept it. tokens is the current list.

–
placeholderstring

Default: "Add a filter…" (the queryPlaceholder label, then queryAddAnother once there are chips).

–
disabledboolean–
namestring

Also submits each token with a native form, under this name.

–
idstring–
refRef<HTMLInputElement>–
onBlur() => void–
classNamestring–
aria-labelstring–
aria-labelledbystring–
MotionChip enters, chip leaves, chips reflow… · reduced motion

Motion

Motion tokens
MomentTokens
Chip entersfades into its slot while the field makes room (PresenceList)
duration.baseease.enter

220ms · 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.exit

140ms · cubic-bezier(0.4, 0, 1, 1)

140mscubic-bezier(0.4, 0, 1, 1)
Chips reflowglide to their new places
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Refusedthe frame shakes a decaying 4, 3 and 2px
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Reasongrows in under the field (Collapse)
duration.baseease.standard

220ms · 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.standard

340ms · 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-label or aria-labelledby, and the chips are a list named "Active filters". A refused token sets aria-invalid and is described by its reason, a role="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.