Token Field
betaA field for entries that each resolve to one real thing, like emails and people. A finished entry becomes a soft token, a known person shows with their photo, and a bad email turns red.
Preview
entries
size
states
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/token-field
Use it for
- Inviting people by email, or adding known people to a team or a message.
- An entry is finished by a comma, Enter, Tab, a paste, or leaving the field. Backspace in the empty field removes the last token.
- Pass people with { name, email, photo }. An entry that matches a person's email or name shows as them. Without a photo it shows their initials.
- An entry with an @ that is not a valid email shows red, and says so in its tooltip and to screen readers. Say what to fix in error.
- Text that is not an email and matches no person stays as text. It never becomes a token.
- value is the list of entries as typed. Check each one on your server too.
Avoid it for
- Free tags or labels people make up. This field only takes entries that resolve to someone or something real.
- Picking from a fixed list. Use a combobox with multiple.
- One email address. Use an input with type="email".
Props
- value
The entries. Use it with onValueChange.
string[]
- defaultValue
The entries at first render, when you do not control them.
string[]
- onValueChange
Called with the new list each time an entry is added or removed.
(tokens: string[]) => void
- people
Known people. A matching entry shows as them, with their photo or initials.
{ name: string; email?: string; photo?: string }[]
- placeholder
Shown while the field is empty. Also names the field when there is no label.
string
- label
Names the field, above it.
ReactNode
- hint
A short note under the field.
ReactNode
- error
Replaces the hint, in red, and is announced.
ReactNode
- size
Height, padding, and text size. The same heights as Input.
"sm" | "md" | "lg" · default "md"
- disabled
Blocks typing, and hides the remove buttons.
boolean · default false
- className
Classes for the frame. Other props, like name and aria-*, land on the input.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| value | string[] | - | The entries. Use it with onValueChange. |
| defaultValue | string[] | - | The entries at first render, when you do not control them. |
| onValueChange | (tokens: string[]) => void | - | Called with the new list each time an entry is added or removed. |
| people | { name: string; email?: string; photo?: string }[] | - | Known people. A matching entry shows as them, with their photo or initials. |
| placeholder | string | - | Shown while the field is empty. Also names the field when there is no label. |
| label | ReactNode | - | Names the field, above it. |
| hint | ReactNode | - | A short note under the field. |
| error | ReactNode | - | Replaces the hint, in red, and is announced. |
| size | "sm" | "md" | "lg" | "md" | Height, padding, and text size. The same heights as Input. |
| disabled | boolean | false | Blocks typing, and hides the remove buttons. |
| className | string | - | Classes for the frame. Other props, like name and aria-*, land on the input. |
Accessibility
- The text field is a native input, named by the label, or by the placeholder when there is no label.
- Each token reads as its name. A bad email adds ", not a valid email address" for screen readers, and a tooltip for the mouse.
- Each token has a remove button named "Remove" plus the token, with a tooltip that says the same. It is not a Tab stop, since Backspace removes from the keyboard.
- A polite live region says what was added or removed.
- The × shows on hover with a mouse, and always on touch screens, where nothing hovers.
- On touch screens the text is 16px, so iOS Safari does not zoom in when the field gets focus.
Examples
Invite by email
<TokenField
label="Invite"
placeholder="Add email addresses"
defaultValue={["dan.kerr@mail.com", "tom.hale@mail.com"]}
hint="Separate them with commas."
/>Known people
<TokenField
label="People"
placeholder="Add people by name or email"
people={[
{ name: "Maya Patel", email: "maya@mail.com" },
{ name: "Tom Hale", email: "tom.hale@mail.com" },
]}
defaultValue={["maya@mail.com"]}
/>