Skip to content

Tag Input

Enter free-form values as chips, with validation and duplicate removal.

import { TagInput } 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 a tag input for values people make up: repository topics, allowed email domains, keywords, recipients. Nothing is chosen from a list. For values from a known list, use a Multi-select, which searches and groups them.

import { Field, FieldLabel, TagInput } from '@youj/react'

export function TopicsField() {
  const [topics, setTopics] = useState(['react', 'motion'])
  return (
    <Field>
      <FieldLabel>Topics</FieldLabel>
      <TagInput
        value={topics}
        onValueChange={setTopics}
        validate={(tag) => (/^[a-z0-9-]+$/.test(tag) ? undefined : 'Use lowercase letters, numbers and hyphens.')}
        maxTags={8}
      />
    </Field>
  )
}

The field is an Input Group whose text input sits inline after the chips and wraps with them. Enter or a comma adds what you typed. delimiters changes the keys besides Enter, and addOnBlur (on by default) adds the text when the field is left.

Reference

Propsvalue, defaultValue, onValueChange… (15)

Props

TagInput props
PropType
valuestring[]–
defaultValuestring[]

default []

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

Returns a reason to reject a tag, or nothing to accept it. tags is the current list.

–
delimitersstring[]

Keys that commit the typed text, besides Enter.

default [',']

[',']
addOnBlurboolean

Adds the typed text when the field loses focus.

default true

true
maxTagsnumber

The most tags; the field stops accepting new ones at the limit.

–
placeholderstring–
disabledboolean–
namestring

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

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

Motion

Motion tokens
MomentTokens
Chip entersfades down into its slot (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
duration.fastease.exit

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

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

0.36s, bounce 0

0.36s, bounce 0
Rejectedthe 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 existing chip 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, leave and reorder in one frame. The duplicate flash and the reason still show, so the feedback survives.

Reduced motion

The rejection doesn't shake and chips don't glide. The duplicate chip still flashes and the reason still appears, because they carry the message rather than decorate it.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The text input is named by its FieldLabel, or by aria-label. While a tag is rejected, it carries aria-invalid and is described by the reason, which is a role="alert" message and is read out at once.
  • In the field, Backspace when empty, or ArrowLeft at the start, reaches the last chip. On a chip, arrows move between chips, Alt with an arrow moves the chip itself, Home and End jump to the ends, Backspace or Delete removes it and focus lands on its neighbor, and Escape returns to the field.
  • Each chip's remove button is named after its tag. Additions, removals, moves and duplicates are announced politely.
  • With name, each tag is submitted with a native form under that name.