Tag input
PortedFree-form values, such as emails or labels, shown as tags in a field. Enter or a comma adds the text as a tag, and so does leaving the field; Backspace in an empty field removes the last tag; pasted text with commas or new lines becomes several tags. validateValue checks each tag and maxValues limits their number; a value turned down stays in the field with a message. Native <input> attributes and ref go to the text field; className goes to the box.
import { TagInput } from "@etched/monet/tag-input";
export function Preview() {
return <TagInput aria-label="Labels" defaultValue={["urgent", "referral"]} placeholder="Add a label" />;
}Installation
import { TagInput } from "@etched/monet/tag-input";Examples
Emails
Enter or a comma adds a tag; Backspace in the empty field removes the last one. Pasted text with commas or new lines becomes several tags. validateValue checks each one: a value that is not an email stays in the field, with a message.
Paste a list, one per line or with commas.
import { useState } from "react";
import { TagInput } from "@etched/monet/tag-input";
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function Emails() {
const [recipients, setRecipients] = useState(["ana@etched.com"]);
return (
<div className="grid">
<TagInput
label="Recipients"
description="Paste a list, one per line or with commas."
placeholder="name@etched.com"
value={recipients}
onValueChange={setRecipients}
validateValue={(value) => email.test(value)}
/>
</div>
);
}A limit
maxValues stops at three tags; the next one stays in the field, with a message.
Up to three groups.
import { TagInput } from "@etched/monet/tag-input";
export function ALimit() {
return (
<div className="grid">
<TagInput label="Access groups" description="Up to three groups." defaultValue={["recruiting"]} maxValues={3} placeholder="Add a group" />
</div>
);
}In a form
With name, each tag is its own form value. required asks for at least one tag, and a value that validateValue turns down stops the submit. A form reset brings back the first tags.
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { TagInput } from "@etched/monet/tag-input";
export function InAForm() {
const [submitted, setSubmitted] = useState<string[] | null>(null);
return (
<form
className="grid"
onSubmit={(event) => {
event.preventDefault();
setSubmitted(new FormData(event.currentTarget).getAll("topics").map(String));
}}
>
<TagInput
name="topics"
required
label="Topics"
description="Up to 20 characters each. Add at least one topic, then submit."
placeholder="Add a topic"
validateValue={(value) => value.length <= 20}
/>
<div className="row">
<Button type="submit" variant="primary">
Submit
</Button>
<Button type="reset">Reset</Button>
{submitted && <span>Topics: {submitted.join(", ")}</span>}
</div>
</form>
);
}States
Read-only shows the tags without remove buttons; the field still takes focus. Disabled fades the field. An error from the app shows under it.
Managed by your admin.
import { TagInput } from "@etched/monet/tag-input";
export function States() {
return (
<div className="grid">
<TagInput label="Assigned teams" description="Managed by your admin." defaultValue={["Recruiting", "Silicon"]} readOnly />
<TagInput label="Watchers" defaultValue={["ana@etched.com"]} disabled />
<TagInput label="Reviewers" placeholder="Add a reviewer" error="Add at least one reviewer." />
</div>
);
}Translated text
removeLabel, invalidMessage and limitMessage give the remove buttons' names and the messages in another language.
import { TagInput } from "@etched/monet/tag-input";
export function TranslatedText() {
return (
<div className="grid">
<TagInput
label="Etiquetas"
defaultValue={["uno"]}
maxValues={2}
placeholder="Agregar una etiqueta"
removeLabel={(value) => `Eliminar ${value}`}
invalidMessage={(value) => `“${value}” no es válido.`}
limitMessage={(maxValues) => `Máximo de ${maxValues} etiquetas.`}
/>
</div>
);
}Accessibility
Enteror a comma adds the text as a tag.Backspacein the empty field removes the last tag;Enterin the empty field submits the form.- A text field named by
label, or byaria-label("Add tag" without one). Each tag has a remove button, named "Remove" and the tag, or byremoveLabel. - When a remove button removes its tag, focus goes back to the field. Leaving the box adds the typed text as tags.
- When
validateValueormaxValuesturns a value down, the field isaria-invalidand the message is analert, so screen readers hear why. - Pass translated text in
aria-label,removeLabel,invalidMessageandlimitMessage.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | – | Visible label. Without one, give the control an aria-label. |
| labelTooltip | ReactNode | – | More about the field, behind an info button after the label. |
| labelTooltipLabel | string | "More information" | Names the info button. |
| description | ReactNode | – | Help text below the control. Hidden while an error shows. |
| error | FieldError | – | |
| value | string[] | – | The tags, when the app holds them. |
| defaultValue | string[] | – | |
| onValueChange | (value: string[]) => void | – | |
| validateValue | (value: string, accepted: string[]) => boolean | – | Return false to turn a value down: it stays in the field, with a message. Gets the tags accepted so far. |
| maxValues | number | – | The most tags it takes. |
| removeLabel | (value: string) => string | `Remove ${value}` | Names a tag's remove button. |
| invalidMessage | (value: string) => string | `“${value}” is not valid.` | Shown when validateValue turns a value down. |
| limitMessage | (maxValues: number) => string | `Limit of ${maxValues} tags reached.` | Shown when a tag would go past maxValues. |
| size | "sm" | "base" | "base" | Same heights as Input while it holds one row of tags. |
| name | string | – | Submits each tag as its own form value under this name. |
| required | boolean | – | Needs at least one tag to submit. Like a native field, a read-only TagInput is not checked. |
| readOnly | boolean | – | Shows the tags, but they cannot change. The field still takes focus. |
| className | string | – | Class on the box around the tags and the field, for layout such as width. |