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.
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.
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.
import { Tag } from '@/ui/tag';
export default function TagDisabled() {
return (
<Tag disabled onRemove={() => {}}>
Maya Chen
</Tag>
);
}Tag as a link
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.
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.
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>
);
}