Monet
@etched/monet 0.0.1

Tag input

Ported

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

Kumo source
urgentreferral
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.

ana@etched.com

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.

recruiting

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.

Up to 20 characters each. Add at least one topic, then submit.

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.

RecruitingSilicon

Managed by your admin.

ana@etched.com
Add at least one reviewer.
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.

uno
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

  • Enter or a comma adds the text as a tag. Backspace in the empty field removes the last tag; Enter in the empty field submits the form.
  • A text field named by label, or by aria-label ("Add tag" without one). Each tag has a remove button, named "Remove" and the tag, or by removeLabel.
  • When a remove button removes its tag, focus goes back to the field. Leaving the box adds the typed text as tags.
  • When validateValue or maxValues turns a value down, the field is aria-invalid and the message is an alert, so screen readers hear why.
  • Pass translated text in aria-label, removeLabel, invalidMessage and limitMessage.

API reference

PropTypeDefaultDescription
labelReactNode–Visible label. Without one, give the control an aria-label.
labelTooltipReactNode–More about the field, behind an info button after the label.
labelTooltipLabelstring"More information"Names the info button.
descriptionReactNode–Help text below the control. Hidden while an error shows.
errorFieldError–
valuestring[]–The tags, when the app holds them.
defaultValuestring[]–
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.
maxValuesnumber–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.
namestring–Submits each tag as its own form value under this name.
requiredboolean–Needs at least one tag to submit. Like a native field, a read-only TagInput is not checked.
readOnlyboolean–Shows the tags, but they cannot change. The field still takes focus.
classNamestring–Class on the box around the tags and the field, for layout such as width.