# Tag

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/tag/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

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.

```ts
import { Tag, TagGroup } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/tag/

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

```tsx
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.

```tsx
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.

```tsx
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.

```tsx
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

- A status that cannot be acted on → Badge
- The user picks several values from a list → MultiSelect

## 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`

| Prop | Type | Default | Description |
|---|---|---|---|
| `tone` | [`TagTone`](https://romanhaendler.github.io/umriss-ui/core/tag/#type-TagTone)<br>`"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.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/tag/#configurator-tag), [Tones](https://romanhaendler.github.io/umriss-ui/core/tag/#tones), [Remove tags from a group](https://romanhaendler.github.io/umriss-ui/core/tag/#removable) and 2 more |
| `children` *required* | `ReactNode` | — | The text of the label – short enough to stand on one line.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/tag/#configurator-tag), [A tag](https://romanhaendler.github.io/umriss-ui/core/tag/#a-tag), [Tones](https://romanhaendler.github.io/umriss-ui/core/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.<br>Shown in: [Remove tags from a group](https://romanhaendler.github.io/umriss-ui/core/tag/#removable) |
| `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.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/tag/#configurator-tag), [Remove tags from a group](https://romanhaendler.github.io/umriss-ui/core/tag/#removable) |
| `ref` | `Ref<HTMLSpanElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onRemove` | `() => void` | — | Makes the tag removable. Without this it is a pure label.<br>Shown in: [Remove tags from a group](https://romanhaendler.github.io/umriss-ui/core/tag/#removable), [Build a filter bar](https://romanhaendler.github.io/umriss-ui/core/tag/#a-filter-bar), [Say why a filter found nothing (EmptyState)](https://romanhaendler.github.io/umriss-ui/core/emptystate/#a-filter-found-nothing) |

Also takes every attribute of `<span>` – without `onRemove`.

### `TagGroupProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `ref` | `Ref<HTMLDivElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

#### Accessibility

| Prop | Type | Default | Description |
|---|---|---|---|
| `aria-label` *required* | `string` | — | Label of the group for the screen reader.<br>Shown in: [Remove tags from a group](https://romanhaendler.github.io/umriss-ui/core/tag/#removable), [Build a filter bar](https://romanhaendler.github.io/umriss-ui/core/tag/#a-filter-bar), [Say why a filter found nothing (EmptyState)](https://romanhaendler.github.io/umriss-ui/core/emptystate/#a-filter-found-nothing) 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.

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

## Known limits

- No field for typing new tags: a tag input is a [MultiSelect](https://romanhaendler.github.io/umriss-ui/core/multiselect/).
- Tags are not toggles: a set of choices to switch on and off is a group of [Checkbox](https://romanhaendler.github.io/umriss-ui/core/checkbox/) or a [ButtonGroup](https://romanhaendler.github.io/umriss-ui/core/buttongroup/).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
