A short label on content that the user can remove where you allow it (also called a chip). Reach for it for active filters, chosen values and assignments.
import { Tag, TagGroup } from "@umriss-ui/core";
onRemove turns the label into a control. In a TagGroup the whole group is one tab stop, and focus moves to the neighbour after a removal instead of falling to the top of the page.
A disabled tag keeps its place and loses its remove button: disabled speaks about the button, not the label. Every tone keeps its contrast in both themes, and the word carries the meaning.
A short label on content – here the depot a vehicle belongs to.
import { Tag } from "@umriss-ui/core";
export default function ATag() {
return <Tag>North depot</Tag>;
}
Every tone keeps its contrast in both themes, and the word beside the colour carries the meaning on its own.
import { Stack, Tag } from "@umriss-ui/core";
export default function Tones() {
return (
<Stack direction="row" gap={2} wrap align="center">
<Tag>Van</Tag>
<Tag tone="accent">E-van</Tag>
<Tag tone="success">On time</Tag>
<Tag tone="warning">Window at risk</Tag>
<Tag tone="danger">Failed attempt</Tag>
</Stack>
);
}
onRemove adds a remove button; in a TagGroup the arrow keys move between tags and focus lands on the neighbour after a removal. A tag whose content is more than text - the on-call mark - needs removeLabel, as no name for its button can be read from markup.
import { useState } from "react";
import { Button, Stack, Tag, TagGroup, Text } from "@umriss-ui/core";
const INITIAL = ["Martin Hale", "Nadia Petrova", "Owen Carter"];
export default function Removable() {
const [drivers, setDrivers] = useState(INITIAL);
const [standby, setStandby] = useState(true);
return (
<Stack gap={3} align="flex-start">
<TagGroup aria-label="Drivers on the early tours">
{drivers.map((driver) => (
<Tag key={driver} tone="accent" onRemove={() => setDrivers((all) => all.filter((d) => d !== driver))}>
{driver}
</Tag>
))}
{standby && (
<Tag tone="accent" removeLabel="Remove Sven Berg (on call)" onRemove={() => setStandby(false)}>
<Text as="span" size="xs" weight="semibold">
On call
</Text>{" "}
Sven Berg
</Tag>
)}
<Tag disabled onRemove={() => {}}>
Lucia Romero (lead)
</Tag>
</TagGroup>
{(drivers.length < INITIAL.length || !standby) && (
<Button
size="sm"
onClick={() => {
setDrivers(INITIAL);
setStandby(true);
}}
>
Reset
</Button>
)}
</Stack>
);
}
Each active filter is a removable tag; the list below follows them, and an empty result offers to clear them all.
import { useState } from "react";
import { Button, EmptyState, Select, Stack, Tag, TagGroup, Text } from "@umriss-ui/core";
interface Shipment {
id: string;
customer: string;
depot: string;
status: "delivered" | "out for delivery" | "failed attempt";
}
const SHIPMENTS: Shipment[] = [
{ id: "SH-1042", customer: "Harlow Bakery", depot: "North depot", status: "delivered" },
{ id: "SH-1043", customer: "Pine & Oak Florist", depot: "North depot", status: "out for delivery" },
{ id: "SH-1051", customer: "Quayside Books", depot: "Riverside depot", status: "failed attempt" },
{ id: "SH-1052", customer: "Moreau Optics", depot: "Riverside depot", status: "out for delivery" },
{ id: "SH-1063", customer: "Linden Dental", depot: "East Gate depot", status: "delivered" },
];
type Filter = { key: "depot" | "status"; value: string };
const OPTIONS: Filter[] = [
...["North depot", "Riverside depot", "East Gate depot"].map((value) => ({ key: "depot" as const, value })),
...["delivered", "out for delivery", "failed attempt"].map((value) => ({ key: "status" as const, value })),
];
export default function AFilterBar() {
const [filters, setFilters] = useState<Filter[]>([{ key: "depot", value: "North depot" }]);
const shown = SHIPMENTS.filter((shipment) =>
(["depot", "status"] as const).every((key) => {
const wanted = filters.filter((f) => f.key === key).map((f) => f.value);
return wanted.length === 0 || wanted.includes(shipment[key]);
}),
);
return (
<Stack gap={3} style={{ maxWidth: 560 }}>
<Stack direction="row" gap={3} align="center" wrap>
<div style={{ width: 200 }}>
<Select
size="sm"
aria-label="Add a filter"
value=""
onChange={(event) => {
const picked = OPTIONS[Number(event.target.value)];
if (picked && !filters.some((f) => f.value === picked.value)) setFilters([...filters, picked]);
}}
>
<option value="" disabled>
Add a filter …
</option>
{OPTIONS.map((option, i) => (
<option key={option.value} value={i}>
{option.key === "depot" ? "Depot" : "Status"}: {option.value}
</option>
))}
</Select>
</div>
<TagGroup aria-label="Active filters">
{filters.map((filter) => (
<Tag key={filter.value} tone="accent" onRemove={() => setFilters(filters.filter((f) => f !== filter))}>
{filter.value}
</Tag>
))}
</TagGroup>
{filters.length > 0 && (
<Button size="sm" variant="ghost" onClick={() => setFilters([])}>
Clear all
</Button>
)}
</Stack>
{shown.length === 0 ? (
<EmptyState
title="No shipments match"
description="Remove a filter to see more."
action={
<Button size="sm" onClick={() => setFilters([])}>
Clear the filters
</Button>
}
/>
) : (
shown.map((shipment) => (
<Stack key={shipment.id} direction="row" gap={3}>
<Text as="span" size="sm" mono>
{shipment.id}
</Text>
<Text as="span" size="sm" style={{ flex: 1 }}>
{shipment.customer}
</Text>
<Text as="span" size="sm" tone="muted">
{shipment.status}
</Text>
</Stack>
))
)}
</Stack>
);
}
| Key | Action |
|---|---|
Tab |
Enters the group on one tag and leaves it on the next Tab. |
← / → or ↑ / ↓ |
Moves to the previous or next tag in the group. |
Home / End |
Moves to the first or last tag. |
Delete or Backspace |
Removes the focused tag and moves focus to its neighbour. |
TagProps| Name | Type | Default | Description |
|---|---|---|---|
tone | TagTone| "neutral" | "neutral" | What the label stands for. The tone is never the only information: the
text beside it says the same thing once more. Shown in: Configurator, Tones, Remove tags from a group and 2 more |
childrenrequired | ReactNode | — | The text of the label – short enough to stand on one line. Shown in: Configurator, A tag, Tones and 5 more |
removeLabel | string | — | Label of the remove key. Needed as soon as the content is not text –
no sentence can be formed out of a node. Shown in: Remove tags from a group |
disabled | boolean | false | Takes the remove key away from the tag without hiding it: disabled is a
statement about the key and not about the label. Shown in: Configurator, Remove tags from a group |
ref | Ref<HTMLSpanElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | Type | Default | Description |
|---|---|---|---|
onRemove | () => void | — | Makes the tag removable. Without this it is a pure label. Shown in: Remove tags from a group, Build a filter bar, Say why a filter found nothing (EmptyState) |
Also takes every attribute of <span> – without onRemove.
TagGroupProps| Name | Type | Default | Description |
|---|---|---|---|
ref | Ref<HTMLDivElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | Type | Default | Description |
|---|---|---|---|
aria-labelrequired | string | — | Label of the group for the screen reader. Shown in: Remove tags from a group, Build a filter bar, Say why a filter found nothing (EmptyState) and 1 more |
Also takes every attribute of <div>.
TagToneWhat a tag stands for - never the only statement, its text says it too.
type TagTone = "neutral" | "accent" | "success" | "warning" | "danger";What umriss deliberately does not build, and why: ADR-0032.