Tag

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.

Examples

A tag

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>;
}

Tones

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>
  );
}

Remove tags from a group

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>
  );
}

Build a filter bar

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>
  );
}

When to use something else

Keyboard

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.

API

TagProps

NameTypeDefaultDescription
toneTagTone
| "neutral"
| "accent"
| "success"
| "warning"
| "danger"
"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

childrenrequiredReactNode—The text of the label – short enough to stand on one line.

Shown in: Configurator, A tag, Tones and 5 more

removeLabelstring—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

disabledbooleanfalseTakes 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

refRef<HTMLSpanElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Events

NameTypeDefaultDescription
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

NameTypeDefaultDescription
refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Accessibility

NameTypeDefaultDescription
aria-labelrequiredstring—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>.

Types on this page

TagTone

What a tag stands for - never the only statement, its text says it too.

type TagTone = "neutral" | "accent" | "success" | "warning" | "danger";

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index