ComponentsData display

Tag

Descriptive metadata someone chose: Inbox tags, skills, sectors, guests on a meeting. An outlined chip with a colour dot. It labels a record; it never says where the record stands.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

Client
States
import { Tag } from '@/ui/tag';<Tag>  Client</Tag>

Colour

Eight dot colours for tags people make, such as Inbox tags. The dot is decoration and the name carries the meaning. Option values from a field, such as skills and sectors, have no dot.

ArchiveClientShared with HalcyonCandidateInvoiceUrgentRetainedWarm introPython
import { Tag } from '@/ui/tag';

export default function TagColours() {
  return (
    <div className="flex max-w-120 flex-wrap items-center gap-100">
      <Tag color="grey">Archive</Tag>
      <Tag color="blue">Client</Tag>
      <Tag color="teal">Shared with Halcyon</Tag>
      <Tag color="green">Candidate</Tag>
      <Tag color="amber">Invoice</Tag>
      <Tag color="brick">Urgent</Tag>
      <Tag color="plum">Retained</Tag>
      <Tag color="pink">Warm intro</Tag>
      <Tag>Python</Tag>
    </div>
  );
}

Removable

Pass onRemove for guests on a meeting and values in a multi-value picker. The × is its own 16px button named "Remove Maya Chen". After a remove, focus moves to the next tag's remove button. Read-only tags leave onRemove out.

  • Maya Chen
  • Daniel Okafor
  • Sara Lund
import { useState } from 'react';
import { Tag } from '@/ui/tag';

export default function TagRemovable() {
  const [guests, setGuests] = useState(['Maya Chen', 'Daniel Okafor', 'Sara Lund']);
  return (
    <ul aria-label="Guests" className="flex flex-wrap items-center gap-100">
      {guests.map((guest) => (
        <li key={guest} className="flex">
          <Tag onRemove={() => setGuests((current) => current.filter((name) => name !== guest))}>{guest}</Tag>
        </li>
      ))}
    </ul>
  );
}

Disabled

A disabled tag dims to 50% and its remove button stops working.

Maya Chen
import { Tag } from '@/ui/tag';

export default function TagDisabled() {
  return (
    <Tag disabled onRemove={() => {}}>
      Maya Chen
    </Tag>
  );
}

A tag that opens something, such as the list filtered by it, renders as a link through render. Hover fills it with the hovered grey; focus draws the 2px brand ring with a 2px gap.

import { Tag } from '@/ui/tag';

export default function TagLink() {
  return (
    <Tag color="blue" render={<a href="#client" />}>
      Client
    </Tag>
  );
}

In a field

Inside a field tags are 20px (size="sm") and wrap. Backspace in the empty input removes the last tag; Enter adds one. In a 36px table cell tags stay on one line with +N: see Tag group.

PythondbtAirflow
import { useState } from 'react';
import { Tag } from '@/ui/tag';

export default function TagInAField() {
  const [skills, setSkills] = useState(['Python', 'dbt', 'Airflow']);
  return (
    <div className="flex w-90 flex-col gap-075">
      <label htmlFor="skill-input" className="text-body-small leading-body-small font-medium text-text">
        Skills
      </label>
      <div role="group" aria-label="Skills" className="flex min-h-7 flex-wrap items-center gap-050 rounded-large bg-surface px-050 py-025 shadow-[inset_0_0_0_1px_var(--color-border-input)] focus-within:shadow-[inset_0_0_0_2px_var(--color-border-focused)]">
        {skills.map((skill) => (
          <Tag key={skill} size="sm" onRemove={() => setSkills((current) => current.filter((s) => s !== skill))}>
            {skill}
          </Tag>
        ))}
        <input
          id="skill-input"
          placeholder="Add a skill"
          className="h-5 w-0 min-w-24 grow bg-transparent ps-050 text-body leading-body text-text outline-none placeholder:text-text-subtle"
          onKeyDown={(event) => {
            const input = event.currentTarget;
            if (event.key === 'Backspace' && !input.value) setSkills((current) => current.slice(0, -1));
            if (event.key === 'Enter' && input.value.trim()) {
              const value = input.value.trim();
              setSkills((current) => (current.includes(value) ? current : [...current, value]));
              input.value = '';
            }
          }}
        />
      </div>
    </div>
  );
}

Long name

A name wider than its space ends in an ellipsis, and hovering shows it in full.

Shared with Halcyon Bank and Northbridge Talent
import { Tag } from '@/ui/tag';

export default function TagLongName() {
  return (
    <div className="w-40">
      <Tag color="teal">Shared with Halcyon Bank and Northbridge Talent</Tag>
    </div>
  );
}

On this page