User Experience (UX)
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>, soaria-labelledbyandaria-labelthat 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
Tabalways 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
| Key | Tag Group |
|---|---|
Tab, Shift+Tab | Moves 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 |
Enter | Follows a link, or presses a remove button |
Space | Presses 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:
- 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. 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.- The previous tag's remove button, when you haven't named one, as the Tag recommends.
- 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,removedLabelandemptyLabel, since screen readers read them all in the page's language. Tabfollows the order of the tags in the DOM, which is the reading order in either direction (WCAG 2.4.3).
UX Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
tag-group/list | Must | Component | The tags are a <ul> with one <li> per tag, which keeps its list role where its bullets are removed | WCAG 1.3.1 (A) | Unit test, ARIA snapshot |
tag-group/name | Must | Both | The list has a name, from label or from a visible heading through aria-labelledby | Open Components, beyond WCAG 1.3.1 (A) | Unit test, Screen reader |
tag-group/tab-stops | Must | Component | Each link and remove button is a stop in the tab order, in order, with no roving focus and no arrow keys | WCAG 2.1.1 (A) | Unit test, Keyboard |
tag-group/focus-after-remove | Must | Component | Once 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 removed | WCAG 2.4.3 (A) | Unit test |
tag-group/announce-removal | Should | Component | Once 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-state | Must | Component | With 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 order | Open Components | Unit test |
tag-group/no-selection | Should | Component | No tag in the group can be selected: there's no aria-selected, aria-pressed, grid or row | Open Components | Unit test |
tag-group/short-groups | Should | Usage | A group holds a handful of tags, or offers a way to clear them all | Open Components | Review |
tag-group/alike-tags | Should | Usage | The tags in a group are all removable or none, and all links or none | Open Components | Review |
tag-group/mirrors-in-rtl | Should | Both | Tags wrap from the start edge, so the first tag sits on the right in right-to-left layouts | Open Components | Review |
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 values | Tag Group |
| Show one label on its own | Tag |
| Turn filters on and off | Toggle buttons |
| Pick any number of options out of a few | Checkbox Group |
| Pick several options out of many, from a list | Select with multiple |
Tab path through a big group, so try to keep groups short.aria-selected or aria-pressed. A filter that turns on and off is a toggle button, and picking any number of options out of a few is a Checkbox Group, which have the right roles and states for it. React Aria's TagGroup can select tags, which is one reason it's a grid.Backspace on a remove button would do the same as Enter. It belongs to fields people type tags into, like the Select, which removes its last tag with Backspace on its closed control.fallbackFocus, like the field the tags came from, and without one, it keeps people in the group, on the previous tag or the empty state, where their next Tab takes them on.