Tag Group

User Experience (UX)

How a tag group behaves for every person, input and setting, from what screen readers hear to where focus goes when a tag is removed.

Now let's look at how a tag group behaves for every person, input and setting. A good tag group is accessible, predictable, designed in every state and adaptive. Each tag in it behaves the way the Tag says, so this page covers what happens between them.

Accessible

Name the list

Tags that belong together are a list, so the Tag Group renders a <ul>, with each tag in an <li> (WCAG 1.3.1). A screen reader says "list, 3 items" as people reach it, so they know how many tags there are before they hear the first one, and can skip the rest of the list if it's not what they're after.

A count alone doesn't say what the tags are, though, so the list needs a name, like "Labels" or "Filters", and screen readers say "list, Labels, 3 items". Give it a label, or, when the page already shows a heading or a word before the tags, point aria-labelledby at it, so people hear what they see:

<span id="topics">Topics</span>
<TagGroup aria-labelledby="topics" tags={topics} />
  • The name goes on the <ul>, so aria-labelledby and aria-label that you pass to the Tag Group land there too, while everything else, like a class, lands on its root.
  • The list carries role="list", which looks like it says nothing new. Safari drops the role from a list whose bullets have been removed, as Scott O'Hara explains, so VoiceOver would read the tags as loose words, with no count, and the role keeps it a list.
  • Our reference implementation warns you in the console when the list has no name.

Keep each tag's own tab stops

Every link and every remove button in the group is a stop in the tab order, in the order they come in, exactly as they are on a lone Tag (WCAG 2.1.1). The group adds no keys of its own, so people use it the way they use every other link and button on the page, and nothing about it needs explaining.

There's no APG pattern for tags. React Aria's TagGroup uses another approach: it renders a grid, as its source shows, which the APG's grid pattern describes as a way to "group a set of interactive elements", where "only one of the focusable elements contained by the grid is included in the page tab sequence", and the arrow keys move between them. That's quicker to get past with many tags, but we don't recommend it here:

  • A Tag can hold both a link and a remove button. In a grid, people need the arrow keys to get from one to the other, and screen readers announce rows and cells for what is a list of words.
  • People have to find out that the arrow keys work, since nothing on the screen says so, while Tab always does.
  • The Select already gives each of its tags' remove buttons a stop, so a page with both works the same way in each.

What it costs is a longer Tab path through a big group, which Keep groups short takes care of.

Contrast and size

  • Each tag keeps the Tag's contrast and hit areas: at least 4.5:1 for its label and at least 24 × 24px to press, and 44px tall on touch screens, as the Tag's Contrast and size explains.
  • Those hit areas reach past a tag shorter than they are, so the group puts enough room between rows that they never cover a tag on the next row (WCAG 2.5.8). Gap and rows has the numbers.
  • The empty state's text has at least 4.5:1 too (WCAG 1.4.3), which our tokens meet at 6.6:1 on white and 8.3:1 on zinc-900.

Show focus

Each tag shows focus on its own link and remove button, as the Tag does. The group draws a focus ring of its own in two places only: on the empty state and on the list, which take focus from a script after a removal, so keyboard users can see where they've landed. It's a 2px outline in --color--focus, the same colour as every other component's, drawn 2px outside the empty state's or the list's edge (WCAG 2.4.7).

Predictable

Keyboard

KeyTag Group
Tab, Shift+TabMoves focus to each tag's link, then its remove button, in order, then out of the group. Plain and disabled tags have no stop, a read-only tag stops only on its link, and the empty state has none
EnterFollows a link, or presses a remove button
SpacePresses a remove button

These are the Tag's own keys. The arrow keys, Home, End, Backspace and Delete do nothing in a Tag Group, since it isn't a grid, as Keep each tag's own tab stops explains.

Move focus when a tag goes

When someone removes a tag, its remove button goes with it. If that button had focus, focus would drop to the top of the page, and keyboard and screen reader users would lose their place (WCAG 2.4.3). So once the tag has gone, the Tag Group moves focus, to the first of these that's there:

  1. The next tag's remove button, so people can go on removing tags with Enter, one after another. A tag after it without a remove button, like a read-only one, is skipped.
  2. fallbackFocus, the element you name, like the field the tags came from or a button that clears them all, when no tag with a remove button comes after it.
  3. The previous tag's remove button, when you haven't named one, as the Tag recommends.
  4. The empty state, once no tags are left. When tags are left but none can be removed, the list itself takes focus, so a screen reader reads its name and count.
  • Open
  • Bug
  • Assigned to me
  • This week
const [filters, setFilters] = useState(ALL);
const reset = useRef<HTMLButtonElement>(null);

<TagGroup
  label="Filters"
  tags={filters}
  removable
  emptyLabel="No filters"
  fallbackFocus={() => reset.current}
  onRemove={(filter) => setFilters(filters.filter((other) => other.value !== filter.value))}
/>
<Button ref={reset} size="xs" variant="ghost" onClick={() => setFilters(ALL)}>Reset</Button>

Focus only moves when it was on the tag that went, or when it was nowhere, like after a click in Safari, which doesn't focus a button you click. If the page moved focus somewhere else first, like into a dialog that asks "Remove this label?", it stays there. Atlassian's tag group does the same in its own words: focus goes "to the next focusable element on the page, which is likely the next tag", and after the last one, to what comes next.

We recommend naming fallbackFocus whenever something sits right after the group that people would want next, like a Reset button or the field they add tags with, since it's where they'd go anyway. Without one, the previous tag's remove button keeps them in the group.

Say what went

A screen reader announces where focus lands, like "Remove Docs, button", but not that a tag disappeared, so people can't tell whether it worked. Once a tag has gone, the Tag Group announces it through a polite live region, like "Bug removed", which screen readers read after what they're saying (WCAG 4.1.3). It's the same live region the Button and the Select use, so announcements from different components never talk over each other.

Only a tag whose remove button was pressed is announced. A tag your app takes away for another reason, like a filter that a Reset button clears, isn't, since the person who pressed Reset already knows. removedLabel changes the wording, like for another language.

Let the page decide what goes

The Tag Group doesn't remove a tag itself. Pressing a remove button fires remove with that tag, and the tag stays until your list of tags drops it, since it's your page that knows what the tag stands for. That lets you ask first, or undo it, and if you keep the tag, focus stays where it is and nothing is announced. Focus moves and the announcement plays only once the tag has gone.

Keep groups short

Every link and remove button is a stop in the tab order, so a group of twenty removable tags is twenty presses of Tab to get past. Try to keep groups to a handful of tags, and when people can build up many, like the filters on a search, give them a way to clear them all at once, like the Reset button in the example above, placed after the group so it's also where focus goes once the last tag has gone.

Keep a group's tags alike

We recommend making every tag in a group the same kind: all removable or none, all links or none. Atlassian says the same, "don't mix and match removable tags with non-removable tags or have only some tags act as links", since people can't tell from a row of tags which ones do what. A read-only tag among removable ones, like a label someone else added, is the exception, and it's best to say nearby why it can't be removed.

Every state

Removable

With removable, every tag in the group gets its remove button, which fires the group's remove with that tag. A tag that's disabled or read-only, on its own or through the group, has none.

Disabled

A disabled group disables every tag in it: each is dimmed to 50%, drops its remove button and turns its link off, as the Tag explains. The list keeps its name and its count, so people can still read what's there.

Read-only

A read-only group drops every remove button and keeps every link, so people can read the tags and follow them, but not remove them, like labels on an issue they can't edit. A single tag can be read-only too, through its entry in tags.

Empty

With no tags, the group shows its empty state in place of the list, like "No tags", or whatever emptyLabel or the empty slot says. It's text, with tabindex="-1", so the group can move focus to it once the last tag has gone and a screen reader reads it, and Tab never stops on it. We recommend an emptyLabel that names what's missing, like "No labels" or "No filters", since it may be all a screen reader user hears.

Long labels

A long label ends in the Tag's ellipsis, so a group's rows keep their height. Each item can shrink to fit the row, so a tag never makes the group wider than what holds it, and the whole label stays in the DOM, where screen readers read it.

Adaptive

Pointer, touch and pen

  • On touch screens, each tag's hit areas reach at least 44px tall, so the group puts rows further apart on coarse pointers (@media (any-pointer: coarse)), the same query the Tag uses, so the two never disagree.
  • Hover is the Tag's own, and only applies to pointers that can actually hover.

Motion

The Tag Group doesn't move. If you'd like a tag to fade out when it's removed, wrap the animation in @media (prefers-reduced-motion: no-preference) (WCAG 2.3.3), and drop the tag from your list once it's done, since that's when the group moves focus.

Forced colours

Windows contrast themes replace every colour with the user's own. Each tag keeps the Tag's border in CanvasText, the empty state's text uses CanvasText, and the focus ring on it and on the list uses Highlight.

Colour scheme

The empty state's colour mixes --color--neutral-text, which sets itself with light-dark(), so the Tag Group follows color-scheme and adapts to your light and dark themes on its own, as the Tag does.

Text size and spacing

The space between rows is in rem, like the Tag's height, so both grow together at 200% text size (WCAG 1.4.4), and tags wrap onto more rows rather than overflowing, with wider letter and word spacing too (WCAG 1.4.12).

Languages and direction

The Tag Group lays its tags out with flexbox, so in right-to-left languages the first tag sits on the right and rows wrap from right to left, and each Tag mirrors itself:

  • خطأ
  • ميزة
  • توثيق
<div dir="rtl" lang="ar">
  <TagGroup
    label="التصنيفات"
    tags={labels}
    removable
    removeLabel={(label) => `إزالة ${label}`}
    removedLabel={(label) => `تمت إزالة ${label}`}
    emptyLabel="لا توجد تصنيفات"
    onRemove={remove}
  />
</div>
  • Translate the list's name, the remove buttons' names, the announcement and the empty state together, with label, removeLabel, removedLabel and emptyLabel, since screen readers read them all in the page's language.
  • Tab follows the order of the tags in the DOM, which is the reading order in either direction (WCAG 2.4.3).

UX Checklist

RuleLevelScopeRequirementBasisCheck
tag-group/listMustComponentThe tags are a <ul> with one <li> per tag, which keeps its list role where its bullets are removedWCAG 1.3.1 (A)Unit test, ARIA snapshot
tag-group/nameMustBothThe list has a name, from label or from a visible heading through aria-labelledbyOpen Components, beyond WCAG 1.3.1 (A)Unit test, Screen reader
tag-group/tab-stopsMustComponentEach link and remove button is a stop in the tab order, in order, with no roving focus and no arrow keysWCAG 2.1.1 (A)Unit test, Keyboard
tag-group/focus-after-removeMustComponentOnce a removed tag has gone, focus that was on it moves to the next tag's remove button, then fallbackFocus, then the previous tag's remove button, then the empty state, or the list when tags are left that can't be removedWCAG 2.4.3 (A)Unit test
tag-group/announce-removalShouldComponentOnce a tag whose remove button was pressed has gone, a polite live region announces it, like "Bug removed"WCAG 4.1.3 (AA)Unit test, Screen reader
tag-group/empty-stateMustComponentWith no tags, the empty state shows in place of the list, and can take focus from a script but isn't a stop in the tab orderOpen ComponentsUnit test
tag-group/no-selectionShouldComponentNo tag in the group can be selected: there's no aria-selected, aria-pressed, grid or rowOpen ComponentsUnit test
tag-group/short-groupsShouldUsageA group holds a handful of tags, or offers a way to clear them allOpen ComponentsReview
tag-group/alike-tagsShouldUsageThe tags in a group are all removable or none, and all links or noneOpen ComponentsReview
tag-group/mirrors-in-rtlShouldBothTags wrap from the start edge, so the first tag sits on the right in right-to-left layoutsOpen ComponentsReview

FAQ

Tag Group or something else?

A tag group shows a set of tags that people can read, follow and remove. For anything else, another component will do a better job:

When you need to…Use
Show a set of labels, topics or picked valuesTag Group
Show one label on its ownTag
Turn filters on and offToggle buttons
Pick any number of options out of a fewCheckbox Group
Pick several options out of many, from a listSelect with multiple
Copyright © 2026