TagGroup
A tag group is a focusable list of labels, categories, keywords, filters, or other items, with support for keyboard navigation, selection, and removal.
install | yarn add react-aria-components |
---|---|
version | 1.0.0-beta.2 |
usage | import {TagGroup, TagList, Tag} from 'react-aria-components' |
Example#
import {TagGroup, TagList, Tag, Label} from 'react-aria-components';
<TagGroup selectionMode="multiple">
<Label>Categories</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
</TagGroup>
import {
Label,
Tag,
TagGroup,
TagList
} from 'react-aria-components';
<TagGroup selectionMode="multiple">
<Label>Categories</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
</TagGroup>
import {
Label,
Tag,
TagGroup,
TagList
} from 'react-aria-components';
<TagGroup selectionMode="multiple">
<Label>
Categories
</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
</TagGroup>
Show CSS
.react-aria-TagGroup {
--border-color: var(--spectrum-gray-600);
--border-color-hovered: var(--spectrum-gray-900);
--background-selected: var(--spectrum-gray-900);
--text-color: var(--spectrum-gray-800);
--text-color-selected: var(--spectrum-gray-50);
--remove-button-color: var(--spectrum-gray-700);
--remove-button-color-hovered: var(--spectrum-gray-900);
--focus-ring-color: slateblue;
--invalid-color: var(--spectrum-global-color-red-600);
display: flex;
flex-direction: column;
gap: 2px;
font-size: small;
[slot=description] {
font-size: 12px;
}
[slot=errorMessage] {
font-size: 12px;
color: var(--invalid-color);
}
}
.react-aria-TagList {
display: flex;
flex-wrap: wrap;
gap: 4px;
.react-aria-Tag {
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 2px 8px;
font-size: 0.929rem;
outline: none;
cursor: default;
display: flex;
align-items: center;
transition: border-color 200ms;
&[data-hovered] {
border-color: var(--border-color-hovered);
}
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
&[data-selected] {
border-color: var(--background-selected);
background: var(--background-selected);
color: var(--text-color-selected);
}
&[data-disabled] {
opacity: 0.4;
}
[slot=remove] {
background: none;
border: none;
padding: 0;
margin-left: 8px;
color: var(--remove-button-color);
transition: color 200ms;
outline: none;
font-size: 0.95em;
&[data-hovered] {
color: var(--remove-button-color-hovered);
}
}
}
}
@media (forced-colors: active) {
.react-aria-TagGroup {
forced-color-adjust: none;
--border-color: ButtonBorder;
--border-color-hovered: ButtonBorder;
--background-selected: Highlight;
--text-color: ButtonText;
--text-color-selected: HighlightText;
--remove-button-color: ButtonText;
--remove-button-color-hovered: Highlight;
--focus-ring-color: Highlight;
--invalid-color: LinkText;
.react-aria-Tag[data-disabled] {
opacity: 1;
--border-color: GrayText;
--border-color-hovered: GrayText;
--text-color: GrayText;
}
}
}
.react-aria-TagGroup {
--border-color: var(--spectrum-gray-600);
--border-color-hovered: var(--spectrum-gray-900);
--background-selected: var(--spectrum-gray-900);
--text-color: var(--spectrum-gray-800);
--text-color-selected: var(--spectrum-gray-50);
--remove-button-color: var(--spectrum-gray-700);
--remove-button-color-hovered: var(--spectrum-gray-900);
--focus-ring-color: slateblue;
--invalid-color: var(--spectrum-global-color-red-600);
display: flex;
flex-direction: column;
gap: 2px;
font-size: small;
[slot=description] {
font-size: 12px;
}
[slot=errorMessage] {
font-size: 12px;
color: var(--invalid-color);
}
}
.react-aria-TagList {
display: flex;
flex-wrap: wrap;
gap: 4px;
.react-aria-Tag {
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 2px 8px;
font-size: 0.929rem;
outline: none;
cursor: default;
display: flex;
align-items: center;
transition: border-color 200ms;
&[data-hovered] {
border-color: var(--border-color-hovered);
}
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
&[data-selected] {
border-color: var(--background-selected);
background: var(--background-selected);
color: var(--text-color-selected);
}
&[data-disabled] {
opacity: 0.4;
}
[slot=remove] {
background: none;
border: none;
padding: 0;
margin-left: 8px;
color: var(--remove-button-color);
transition: color 200ms;
outline: none;
font-size: 0.95em;
&[data-hovered] {
color: var(--remove-button-color-hovered);
}
}
}
}
@media (forced-colors: active) {
.react-aria-TagGroup {
forced-color-adjust: none;
--border-color: ButtonBorder;
--border-color-hovered: ButtonBorder;
--background-selected: Highlight;
--text-color: ButtonText;
--text-color-selected: HighlightText;
--remove-button-color: ButtonText;
--remove-button-color-hovered: Highlight;
--focus-ring-color: Highlight;
--invalid-color: LinkText;
.react-aria-Tag[data-disabled] {
opacity: 1;
--border-color: GrayText;
--border-color-hovered: GrayText;
--text-color: GrayText;
}
}
}
.react-aria-TagGroup {
--border-color: var(--spectrum-gray-600);
--border-color-hovered: var(--spectrum-gray-900);
--background-selected: var(--spectrum-gray-900);
--text-color: var(--spectrum-gray-800);
--text-color-selected: var(--spectrum-gray-50);
--remove-button-color: var(--spectrum-gray-700);
--remove-button-color-hovered: var(--spectrum-gray-900);
--focus-ring-color: slateblue;
--invalid-color: var(--spectrum-global-color-red-600);
display: flex;
flex-direction: column;
gap: 2px;
font-size: small;
[slot=description] {
font-size: 12px;
}
[slot=errorMessage] {
font-size: 12px;
color: var(--invalid-color);
}
}
.react-aria-TagList {
display: flex;
flex-wrap: wrap;
gap: 4px;
.react-aria-Tag {
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 2px 8px;
font-size: 0.929rem;
outline: none;
cursor: default;
display: flex;
align-items: center;
transition: border-color 200ms;
&[data-hovered] {
border-color: var(--border-color-hovered);
}
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
&[data-selected] {
border-color: var(--background-selected);
background: var(--background-selected);
color: var(--text-color-selected);
}
&[data-disabled] {
opacity: 0.4;
}
[slot=remove] {
background: none;
border: none;
padding: 0;
margin-left: 8px;
color: var(--remove-button-color);
transition: color 200ms;
outline: none;
font-size: 0.95em;
&[data-hovered] {
color: var(--remove-button-color-hovered);
}
}
}
}
@media (forced-colors: active) {
.react-aria-TagGroup {
forced-color-adjust: none;
--border-color: ButtonBorder;
--border-color-hovered: ButtonBorder;
--background-selected: Highlight;
--text-color: ButtonText;
--text-color-selected: HighlightText;
--remove-button-color: ButtonText;
--remove-button-color-hovered: Highlight;
--focus-ring-color: Highlight;
--invalid-color: LinkText;
.react-aria-Tag[data-disabled] {
opacity: 1;
--border-color: GrayText;
--border-color-hovered: GrayText;
--text-color: GrayText;
}
}
}
Features#
A static tag list can be built using <ul> or <ol> HTML elements, but does not support any user interactions.
HTML lists are meant for static content, rather than lists with rich interactions such as keyboard navigation, item selection, removal, etc.
TagGroup
helps achieve accessible and interactive tag list components that can be styled as needed.
- Keyboard navigation – Tags can be navigated using the arrow keys, along with page up/down, home/end, etc.
- Removable – Tags can be removed from the tag group by clicking a remove button or pressing the backspace key.
- Item selection – Single or multiple selection, with support for disabled items and both
toggle
andreplace
selection behaviors. - Accessible – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent.
- Styleable – Items include builtin states for styling, such as hover, press, focus, selected, and disabled.
Anatomy#
A tag group consists of label and a list of tags. Each tag should include a visual label, and may optionally include a remove button. If a visual label is not included in a tag group, then an aria-label
or aria-labelledby
prop must be passed to identify it to assistive technology.
TagGroup
also supports optional description and error message slots, which can be used
to provide more context about the tag group, and any validation messages. These are linked with the
tag group via the aria-describedby
attribute.
import {Button, Label, Tag, TagGroup, TagList, Text} from 'react-aria-components';
<TagGroup>
<Label />
<TagList>
<Tag>
<Button slot="remove" />
</Tag>
</TagList>
<Text slot="description" />
<Text slot="errorMessage" />
</TagGroup>
import {
Button,
Label,
Tag,
TagGroup,
TagList,
Text
} from 'react-aria-components';
<TagGroup>
<Label />
<TagList>
<Tag>
<Button slot="remove" />
</Tag>
</TagList>
<Text slot="description" />
<Text slot="errorMessage" />
</TagGroup>
import {
Button,
Label,
Tag,
TagGroup,
TagList,
Text
} from 'react-aria-components';
<TagGroup>
<Label />
<TagList>
<Tag>
<Button slot="remove" />
</Tag>
</TagList>
<Text slot="description" />
<Text slot="errorMessage" />
</TagGroup>
Concepts#
TagGroup
makes use of the following concepts:
Composed components#
A TagGroup
uses the following components, which may also be used standalone or reused in other components.
Reusable wrappers#
If you will use a TagGroup in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency.
This example wraps TagGroup
and all of its children together into a single component which accepts a label
prop and children
, which are passed through to the right places. It also shows how to use the description
and errorMessage
slots to render help text (see below for details). The Tag
component is also wrapped to automatically render a remove button when the onRemove
prop is provided to the TagGroup.
import type {TagGroupProps, TagListProps, TagProps} from 'react-aria-components';
import {Button, Text} from 'react-aria-components';
interface MyTagGroupProps<T>
extends
Omit<TagGroupProps, 'children'>,
Pick<TagListProps<T>, 'items' | 'children' | 'renderEmptyState'> {
label?: string;
description?: string;
errorMessage?: string;
}
function MyTagGroup<T extends object>(
{
label,
description,
errorMessage,
items,
children,
renderEmptyState,
...props
}: MyTagGroupProps<T>
) {
return (
<TagGroup {...props}>
<Label>{label}</Label>
<TagList items={items} renderEmptyState={renderEmptyState}>
{children}
</TagList>
{description && <Text slot="description">{description}</Text>}
{errorMessage && <Text slot="errorMessage">{errorMessage}</Text>}
</TagGroup>
);
}
function MyTag({ children, ...props }: TagProps) {
let textValue = typeof children === 'string' ? children : undefined;
return (
<Tag textValue={textValue} {...props}>
{({ allowsRemoving }) => (
<>
{children}
{allowsRemoving && <Button slot="remove">ⓧ</Button>}
</>
)}
</Tag>
);
}
<MyTagGroup label="Ice cream flavor" selectionMode="single">
<MyTag>Chocolate</MyTag>
<MyTag>Mint</MyTag>
<MyTag>Strawberry</MyTag>
<MyTag>Vanilla</MyTag>
</MyTagGroup>
import type {
TagGroupProps,
TagListProps,
TagProps
} from 'react-aria-components';
import {Button, Text} from 'react-aria-components';
interface MyTagGroupProps<T>
extends
Omit<TagGroupProps, 'children'>,
Pick<
TagListProps<T>,
'items' | 'children' | 'renderEmptyState'
> {
label?: string;
description?: string;
errorMessage?: string;
}
function MyTagGroup<T extends object>(
{
label,
description,
errorMessage,
items,
children,
renderEmptyState,
...props
}: MyTagGroupProps<T>
) {
return (
<TagGroup {...props}>
<Label>{label}</Label>
<TagList
items={items}
renderEmptyState={renderEmptyState}
>
{children}
</TagList>
{description && (
<Text slot="description">{description}</Text>
)}
{errorMessage && (
<Text slot="errorMessage">{errorMessage}</Text>
)}
</TagGroup>
);
}
function MyTag({ children, ...props }: TagProps) {
let textValue = typeof children === 'string'
? children
: undefined;
return (
<Tag textValue={textValue} {...props}>
{({ allowsRemoving }) => (
<>
{children}
{allowsRemoving && (
<Button slot="remove">ⓧ</Button>
)}
</>
)}
</Tag>
);
}
<MyTagGroup
label="Ice cream flavor"
selectionMode="single"
>
<MyTag>Chocolate</MyTag>
<MyTag>Mint</MyTag>
<MyTag>Strawberry</MyTag>
<MyTag>Vanilla</MyTag>
</MyTagGroup>
import type {
TagGroupProps,
TagListProps,
TagProps
} from 'react-aria-components';
import {
Button,
Text
} from 'react-aria-components';
interface MyTagGroupProps<
T
> extends
Omit<
TagGroupProps,
'children'
>,
Pick<
TagListProps<T>,
| 'items'
| 'children'
| 'renderEmptyState'
> {
label?: string;
description?: string;
errorMessage?: string;
}
function MyTagGroup<
T extends object
>(
{
label,
description,
errorMessage,
items,
children,
renderEmptyState,
...props
}: MyTagGroupProps<T>
) {
return (
<TagGroup {...props}>
<Label>
{label}
</Label>
<TagList
items={items}
renderEmptyState={renderEmptyState}
>
{children}
</TagList>
{description && (
<Text slot="description">
{description}
</Text>
)}
{errorMessage && (
<Text slot="errorMessage">
{errorMessage}
</Text>
)}
</TagGroup>
);
}
function MyTag(
{ children, ...props }:
TagProps
) {
let textValue =
typeof children ===
'string'
? children
: undefined;
return (
<Tag
textValue={textValue}
{...props}
>
{(
{
allowsRemoving
}
) => (
<>
{children}
{allowsRemoving &&
(
<Button slot="remove">
ⓧ
</Button>
)}
</>
)}
</Tag>
);
}
<MyTagGroup
label="Ice cream flavor"
selectionMode="single"
>
<MyTag>
Chocolate
</MyTag>
<MyTag>Mint</MyTag>
<MyTag>
Strawberry
</MyTag>
<MyTag>
Vanilla
</MyTag>
</MyTagGroup>
Removing tags#
The onRemove
prop can be used to allow the user to remove tags. In the above example, an additional <Button slot="remove>
element is rendered when a tag group allows removing. The user can also press the backspace key while a tag is focused to remove the tag from the group. Additionally, when selection is enabled, all selected items will be deleted when pressing the backspace key on a selected tag.
import {useListData} from '@react-stately/data';
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: "News" },
{ id: 2, name: "Travel" },
{ id: 3, name: "Gaming" },
{ id: 4, name: "Shopping" }
]
});
return (
<MyTagGroup
label="Categories"
items={list.items}
onRemove={keys => list.remove(...keys)} >
{(item) => <MyTag>{item.name}</MyTag>}
</MyTagGroup>
);
}
import {useListData} from '@react-stately/data';
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: "News" },
{ id: 2, name: "Travel" },
{ id: 3, name: "Gaming" },
{ id: 4, name: "Shopping" }
]
});
return (
<MyTagGroup
label="Categories"
items={list.items}
onRemove={keys => list.remove(...keys)} >
{(item) => <MyTag>{item.name}</MyTag>}
</MyTagGroup>
);
}
import {useListData} from '@react-stately/data';
function Example() {
let list = useListData(
{
initialItems: [
{
id: 1,
name: 'News'
},
{
id: 2,
name: 'Travel'
},
{
id: 3,
name: 'Gaming'
},
{
id: 4,
name:
'Shopping'
}
]
}
);
return (
<MyTagGroup
label="Categories"
items={list.items}
onRemove={(keys) =>
list.remove(
...keys
)} >
{(item) => (
<MyTag>
{item.name}
</MyTag>
)}
</MyTagGroup>
);
}
Selection#
TagGroup supports multiple selection modes. By default, selection is disabled, however this can be changed using the selectionMode
prop.
Use defaultSelectedKeys
to provide a default set of selected items (uncontrolled) and selectedKeys
to set the selected items (controlled). The value of the selected keys must match the id
prop of the items.
See the react-stately
Selection docs for more details.
import type {Selection} from 'react-aria-components';
function Example() {
let [selected, setSelected] = React.useState<Selection>(new Set(['parking']));
return (
<>
<MyTagGroup
label="Amenities"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected} >
<MyTag id="laundry">Laundry</MyTag>
<MyTag id="fitness">Fitness center</MyTag>
<MyTag id="parking">Parking</MyTag>
<MyTag id="pool">Swimming pool</MyTag>
<MyTag id="breakfast">Breakfast</MyTag>
</MyTagGroup>
<p>
Current selection (controlled):{' '}
{selected === 'all' ? 'all' : [...selected].join(', ')}
</p>
</>
);
}
import type {Selection} from 'react-aria-components';
function Example() {
let [selected, setSelected] = React.useState<Selection>(
new Set(['parking'])
);
return (
<>
<MyTagGroup
label="Amenities"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected} >
<MyTag id="laundry">Laundry</MyTag>
<MyTag id="fitness">Fitness center</MyTag>
<MyTag id="parking">Parking</MyTag>
<MyTag id="pool">Swimming pool</MyTag>
<MyTag id="breakfast">Breakfast</MyTag>
</MyTagGroup>
<p>
Current selection (controlled): {selected === 'all'
? 'all'
: [...selected].join(', ')}
</p>
</>
);
}
import type {Selection} from 'react-aria-components';
function Example() {
let [
selected,
setSelected
] = React.useState<
Selection
>(
new Set(['parking'])
);
return (
<>
<MyTagGroup
label="Amenities"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected} >
<MyTag id="laundry">
Laundry
</MyTag>
<MyTag id="fitness">
Fitness center
</MyTag>
<MyTag id="parking">
Parking
</MyTag>
<MyTag id="pool">
Swimming pool
</MyTag>
<MyTag id="breakfast">
Breakfast
</MyTag>
</MyTagGroup>
<p>
Current selection
(controlled):
{' '}
{selected ===
'all'
? 'all'
: [...selected]
.join(', ')}
</p>
</>
);
}
Links#
Tags may be links to another page or website. This can be achieved by passing the href
prop to the <Tag>
component. Tags with an href
are not selectable.
<MyTagGroup label="Links">
<MyTag href="https://adobe.com/" target="_blank">Adobe</MyTag>
<MyTag href="https://apple.com/" target="_blank">Apple</MyTag>
<MyTag href="https://google.com/" target="_blank">Google</MyTag>
<MyTag href="https://microsoft.com/" target="_blank">Microsoft</MyTag>
</MyTagGroup>
<MyTagGroup label="Links">
<MyTag href="https://adobe.com/" target="_blank">
Adobe
</MyTag>
<MyTag href="https://apple.com/" target="_blank">
Apple
</MyTag>
<MyTag href="https://google.com/" target="_blank">
Google
</MyTag>
<MyTag href="https://microsoft.com/" target="_blank">
Microsoft
</MyTag>
</MyTagGroup>
<MyTagGroup label="Links">
<MyTag
href="https://adobe.com/"
target="_blank"
>
Adobe
</MyTag>
<MyTag
href="https://apple.com/"
target="_blank"
>
Apple
</MyTag>
<MyTag
href="https://google.com/"
target="_blank"
>
Google
</MyTag>
<MyTag
href="https://microsoft.com/"
target="_blank"
>
Microsoft
</MyTag>
</MyTagGroup>
Client side routing#
The <Tag>
component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the RouterProvider
component at the root of your app. See the client side routing guide to learn how to set this up.
Disabled tags#
TagGroup supports marking items as disabled using the disabledKeys
prop. Each key in this list
corresponds with the id
prop passed to the Tag
component, or automatically derived from the values passed
to the items
prop. Disabled items are not focusable, selectable, or keyboard navigable.
See Collections for more details.
<MyTagGroup
label="Sandwich contents"
selectionMode="multiple"
disabledKeys={['tuna']}>
<MyTag id="lettuce">Lettuce</MyTag>
<MyTag id="tomato">Tomato</MyTag>
<MyTag id="cheese">Cheese</MyTag>
<MyTag id="tuna">Tuna Salad</MyTag>
<MyTag id="egg">Egg Salad</MyTag>
<MyTag id="ham">Ham</MyTag>
</MyTagGroup>
<MyTagGroup
label="Sandwich contents"
selectionMode="multiple"
disabledKeys={['tuna']}>
<MyTag id="lettuce">Lettuce</MyTag>
<MyTag id="tomato">Tomato</MyTag>
<MyTag id="cheese">Cheese</MyTag>
<MyTag id="tuna">Tuna Salad</MyTag>
<MyTag id="egg">Egg Salad</MyTag>
<MyTag id="ham">Ham</MyTag>
</MyTagGroup>
<MyTagGroup
label="Sandwich contents"
selectionMode="multiple"
disabledKeys={[
'tuna'
]}>
<MyTag id="lettuce">
Lettuce
</MyTag>
<MyTag id="tomato">
Tomato
</MyTag>
<MyTag id="cheese">
Cheese
</MyTag>
<MyTag id="tuna">
Tuna Salad
</MyTag>
<MyTag id="egg">
Egg Salad
</MyTag>
<MyTag id="ham">
Ham
</MyTag>
</MyTagGroup>
Empty state#
Use the renderEmptyState
prop to customize what a TagList
will display if there are no items.
<TagGroup>
<Label>Categories</Label>
<TagList renderEmptyState={() => 'No categories.'}> {[]}
</TagList>
</TagGroup>
<TagGroup>
<Label>Categories</Label>
<TagList renderEmptyState={() => 'No categories.'}> {[]}
</TagList>
</TagGroup>
<TagGroup>
<Label>
Categories
</Label>
<TagList
renderEmptyState={() =>
'No categories.'}
> {[]}
</TagList>
</TagGroup>
Help text#
Description#
The description
slot can be used to associate additional help text with a TagGroup
.
<TagGroup>
<Label>Categories</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<Text slot="description">Your selected categories.</Text></TagGroup>
<TagGroup>
<Label>Categories</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<Text slot="description">Your selected categories.</Text></TagGroup>
<TagGroup>
<Label>
Categories
</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<Text slot="description">
Your selected
categories.
</Text></TagGroup>
Error message#
The errorMessage
slot can be used to help the user fix a validation error.
<TagGroup>
<Label>Categories</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<Text slot="errorMessage">Invalid set of categories.</Text></TagGroup>
<TagGroup>
<Label>Categories</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<Text slot="errorMessage">
Invalid set of categories.
</Text></TagGroup>
<TagGroup>
<Label>
Categories
</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<Text slot="errorMessage">
Invalid set of
categories.
</Text></TagGroup>
Props#
TagGroup#
Name | Type | Description |
selectionBehavior | SelectionBehavior | How multiple selection should behave in the collection. |
disabledKeys | Iterable<Key> | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. |
selectionMode | SelectionMode | The type of selection that is allowed in the collection. |
disallowEmptySelection | boolean | Whether the collection allows empty selection. |
selectedKeys | 'all' | Iterable<Key> | The currently selected keys in the collection (controlled). |
defaultSelectedKeys | 'all' | Iterable<Key> | The initial selected keys in the collection (uncontrolled). |
children | ReactNode | The children of the component. |
className | string | The CSS className for the element. |
style | CSSProperties | The inline style for the element. |
Events
Name | Type | Description |
onRemove | (
(keys: Set<Key>
)) => void | Handler that is called when a user deletes a tag. |
onSelectionChange | (
(keys: Selection
)) => any | Handler that is called when the selection changes. |
Layout
Name | Type | Description |
slot | string | null | A slot name for the component. Slots allow the component to receive props from a parent component.
An explicit |
Accessibility
Name | Type | Description |
id | string | The element's unique identifier. See MDN. |
aria-label | string | Defines a string value that labels the current element. |
aria-labelledby | string | Identifies the element (or elements) that labels the current element. |
aria-describedby | string | Identifies the element (or elements) that describes the object. |
aria-details | string | Identifies the element (or elements) that provide a detailed, extended description for the object. |
TagList#
A <TagList>
is a list of tags within a <TagGroup>
.
Name | Type | Description |
renderEmptyState | (
(props: TagListRenderProps
)) => ReactNode | Provides content to display when there are no items in the tag list. |
children | ReactNode | (
(item: T
)) => ReactNode | The contents of the collection. |
items | Iterable<T> | Item objects in the collection. |
className | string | (
(values: TagListRenderProps
)) => string | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: TagListRenderProps
)) => CSSProperties | The inline style for the element. A function may be provided to compute the style based on component state. |
Tag#
An <Tag>
defines a single item within a <TagList>
. If the children are not plain text, then the textValue
prop must also be set to a plain text representation for accessibility.
Name | Type | Description |
textValue | string | A string representation of the tags's contents, used for accessibility. Required if children is not a plain text string. |
children | ReactNode | (
(values: TagRenderProps
)) => ReactNode | The children of the component. A function may be provided to alter the children based on component state. |
className | string | (
(values: TagRenderProps
)) => string | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: TagRenderProps
)) => CSSProperties | The inline style for the element. A function may be provided to compute the style based on component state. |
href | string | A URL to link to. See MDN. |
target | HTMLAttributeAnchorTarget | The target window for the link. See MDN. |
rel | string | The relationship between the linked resource and the current page. See MDN. |
download | boolean | string | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. |
ping | string | A space-separated list of URLs to ping when the link is followed. See MDN. |
referrerPolicy | HTMLAttributeReferrerPolicy | How much of the referrer to send when following the link. See MDN. |
Accessibility
Name | Type | Description |
id | Key | A unique id for the tag. |
Label#
A <Label>
accepts all HTML attributes.
Text#
<Text>
accepts all HTML attributes.
Styling#
React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className
attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName
naming convention.
.react-aria-TagGroup {
/* ... */
}
.react-aria-TagGroup {
/* ... */
}
.react-aria-TagGroup {
/* ... */
}
A custom className
can also be specified on any component. This overrides the default className
provided by React Aria with your own.
<TagGroup className="my-tag-group">
{/* ... */}
</TagGroup>
<TagGroup className="my-tag-group">
{/* ... */}
</TagGroup>
<TagGroup className="my-tag-group">
{/* ... */}
</TagGroup>
In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example:
.react-aria-Tag[data-selected] {
/* ... */
}
.react-aria-Tag[data-focused] {
/* ... */
}
.react-aria-Tag[data-selected] {
/* ... */
}
.react-aria-Tag[data-focused] {
/* ... */
}
.react-aria-Tag[data-selected] {
/* ... */
}
.react-aria-Tag[data-focused] {
/* ... */
}
The className
and style
props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.
<Tag
className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
Item
</Tag>
<Tag
className={({ isSelected }) =>
isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
Item
</Tag>
<Tag
className={(
{ isSelected }
) =>
isSelected
? 'bg-blue-400'
: 'bg-gray-100'}
>
Item
</Tag>
Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render a remove button only when removal is allowed.
<Tag>
{({allowsRemoving}) => (
<>
Item
{allowsRemoving && <RemoveButton />}
</>
)}
</Tag>
<Tag>
{({allowsRemoving}) => (
<>
Item
{allowsRemoving && <RemoveButton />}
</>
)}
</Tag>
<Tag>
{(
{ allowsRemoving }
) => (
<>
Item
{allowsRemoving &&
(
<RemoveButton />
)}
</>
)}
</Tag>
The states and selectors for each component used in a TagGroup
are documented below.
TagGroup#
A TagGroup
can be targeted with the .react-aria-TagGroup
CSS selector, or by overriding with a custom className
.
TagList#
A TagList
can be targeted with the .react-aria-TagList
CSS selector, or by overriding with a custom className
. It supports the following states and render props:
Name | CSS Selector | Description |
isEmpty | [data-empty] | Whether the tag list has no items and should display its empty state. |
isFocused | [data-focused] | Whether the tag list is currently focused. |
isFocusVisible | [data-focus-visible] | Whether the tag list is currently keyboard focused. |
state | — | State of the TagGroup. |
Tag#
A Tag
can be targeted with the .react-aria-Tag
CSS selector, or by overriding with a custom className
. It supports the following states and render props:
Name | CSS Selector | Description |
allowsRemoving | [data-allows-removing] | Whether the tag group allows items to be removed. |
isHovered | [data-hovered] | Whether the item is currently hovered with a mouse. |
isPressed | [data-pressed] | Whether the item is currently in a pressed state. |
isSelected | [data-selected] | Whether the item is currently selected. |
isFocused | [data-focused] | Whether the item is currently focused. |
isFocusVisible | [data-focus-visible] | Whether the item is currently keyboard focused. |
isDisabled | [data-disabled] | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may
not be focused. Dependent on |
selectionMode | [data-selection-mode="single | multiple"] | The type of selection that is allowed in the collection. |
selectionBehavior | — | The selection behavior for the collection. |
Tags also accept a <Button slot="remove">
element as a child, which allows them to be removed. This can be conditionally rendered using the allowsRemoving
render prop, as shown below.
Label#
A Label
can be targeted with the .react-aria-Label
CSS selector, or by overriding with a custom className
.
Text#
The help text elements within a TagGroup
can be targeted with the [slot=description]
and [slot=errorMessage]
CSS selectors, or by adding a custom className
.
Advanced customization#
Composition#
If you need to customize one of the components within a TagGroup
, such as TagList
or Tag
, in many cases you can create a wrapper component. This lets you customize the props passed to the component.
function MyTag(props) {
return <Tag {...props} className="my-tag" />
}
function MyTag(props) {
return <Tag {...props} className="my-tag" />
}
function MyTag(props) {
return (
<Tag
{...props}
className="my-tag"
/>
);
}
Contexts#
All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps).
Component | Context | Props | Ref |
TagGroup | TagGroupContext | TagGroupProps | HTMLDivElement |
This example shows a component that accepts a TagGroup
and a ToggleButton as children, and allows the user to turn edit mode for the tag group on and off by pressing the button.
import {ToggleButtonContext, TagGroupContext} from 'react-aria-components';
function Removable({children, onRemove}) {
let [isSelected, onChange] = React.useState(false);
return (
<ToggleButtonContext.Provider value={{isSelected, onChange}}>
<TagGroupContext.Provider value={{onRemove: isSelected && onRemove}}> {children}
</TagGroupContext.Provider>
</ToggleButtonContext.Provider>
);
}
import {
TagGroupContext,
ToggleButtonContext
} from 'react-aria-components';
function Removable({ children, onRemove }) {
let [isSelected, onChange] = React.useState(false);
return (
<ToggleButtonContext.Provider
value={{ isSelected, onChange }}
>
<TagGroupContext.Provider
value={{ onRemove: isSelected && onRemove }}
> {children}
</TagGroupContext.Provider>
</ToggleButtonContext.Provider>
);
}
import {
TagGroupContext,
ToggleButtonContext
} from 'react-aria-components';
function Removable(
{ children, onRemove }
) {
let [
isSelected,
onChange
] = React.useState(
false
);
return (
<ToggleButtonContext.Provider
value={{
isSelected,
onChange
}}
>
<TagGroupContext.Provider
value={{
onRemove:
isSelected &&
onRemove
}}
> {children}
</TagGroupContext.Provider>
</ToggleButtonContext.Provider>
);
}
The Removable
component can be reused to make the edit mode of any nested TagGroup
controlled by a ToggleButton
.
import {ToggleButton} from 'react-aria-components';
<Removable onRemove={ids => alert(`Remove `)}>
<MyTagGroup label="Ice cream flavor">
<MyTag id="chocolate">Chocolate</MyTag>
<MyTag id="mint">Mint</MyTag>
<MyTag id="strawberry">Strawberry</MyTag>
<MyTag id="vanilla">Vanilla</MyTag>
</MyTagGroup>
<ToggleButton>Edit</ToggleButton>
</Removable>
import {ToggleButton} from 'react-aria-components';
<Removable onRemove={ids => alert(`Remove `)}>
<MyTagGroup label="Ice cream flavor">
<MyTag id="chocolate">Chocolate</MyTag>
<MyTag id="mint">Mint</MyTag>
<MyTag id="strawberry">Strawberry</MyTag>
<MyTag id="vanilla">Vanilla</MyTag>
</MyTagGroup>
<ToggleButton>Edit</ToggleButton>
</Removable>
import {ToggleButton} from 'react-aria-components';
<Removable
onRemove={(ids) =>
alert(
`Remove
`)}
>
<MyTagGroup label="Ice cream flavor">
<MyTag id="chocolate">
Chocolate
</MyTag>
<MyTag id="mint">
Mint
</MyTag>
<MyTag id="strawberry">
Strawberry
</MyTag>
<MyTag id="vanilla">
Vanilla
</MyTag>
</MyTagGroup>
<ToggleButton>
Edit
</ToggleButton>
</Removable>
Show CSS
.react-aria-ToggleButton {
--deselected-border-color: var(--spectrum-gray-400);
--deselected-border-color-pressed: var(--spectrum-gray-500);
--deselected-background-color: var(--spectrum-gray-50);
--deselected-background-color-pressed: var(--spectrum-gray-100);
--selected-color: var(--spectrum-gray-800);
--selected-color-pressed: var(--spectrum-gray-900);
--text-color: var(--spectrum-alias-text-color);
--text-color-selected: var(--spectrum-gray-50);
color: var(--text-color);
background: var(--deselected-background-color);
border: 1px solid var(--deselected-border-color);
border-radius: 4px;
appearance: none;
vertical-align: middle;
font-size: 1rem;
text-align: center;
margin: 8px 0 0 0;
outline: none;
padding: 4px 12px;
&[data-pressed] {
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--deselected-background-color-pressed);
border-color: var(--deselected-border-color-pressed);
}
&[aria-pressed=true] {
background: var(--selected-color);
border-color: var(--selected-color);
color: var(--text-color-selected);
&[data-pressed] {
background: var(--selected-color-pressed);
border-color: var(--selected-color-pressed);
}
}
&[data-focus-visible] {
border-color: var(--selected-color);
box-shadow: 0 0 0 1px var(--selected-color);
}
}
@media (forced-colors: active) {
.react-aria-ToggleButton {
forced-color-adjust: none;
--deselected-border-color: ButtonBorder;
--deselected-border-color-pressed: ButtonBorder;
--deselected-background-color: ButtonFace;
--deselected-background-color-pressed: ButtonFace;
--selected-color: Highlight;
--selected-color-pressed: Highlight;
--text-color: ButtonText;
--text-color-selected: HighlightText;
}
}
.react-aria-ToggleButton {
--deselected-border-color: var(--spectrum-gray-400);
--deselected-border-color-pressed: var(--spectrum-gray-500);
--deselected-background-color: var(--spectrum-gray-50);
--deselected-background-color-pressed: var(--spectrum-gray-100);
--selected-color: var(--spectrum-gray-800);
--selected-color-pressed: var(--spectrum-gray-900);
--text-color: var(--spectrum-alias-text-color);
--text-color-selected: var(--spectrum-gray-50);
color: var(--text-color);
background: var(--deselected-background-color);
border: 1px solid var(--deselected-border-color);
border-radius: 4px;
appearance: none;
vertical-align: middle;
font-size: 1rem;
text-align: center;
margin: 8px 0 0 0;
outline: none;
padding: 4px 12px;
&[data-pressed] {
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--deselected-background-color-pressed);
border-color: var(--deselected-border-color-pressed);
}
&[aria-pressed=true] {
background: var(--selected-color);
border-color: var(--selected-color);
color: var(--text-color-selected);
&[data-pressed] {
background: var(--selected-color-pressed);
border-color: var(--selected-color-pressed);
}
}
&[data-focus-visible] {
border-color: var(--selected-color);
box-shadow: 0 0 0 1px var(--selected-color);
}
}
@media (forced-colors: active) {
.react-aria-ToggleButton {
forced-color-adjust: none;
--deselected-border-color: ButtonBorder;
--deselected-border-color-pressed: ButtonBorder;
--deselected-background-color: ButtonFace;
--deselected-background-color-pressed: ButtonFace;
--selected-color: Highlight;
--selected-color-pressed: Highlight;
--text-color: ButtonText;
--text-color-selected: HighlightText;
}
}
.react-aria-ToggleButton {
--deselected-border-color: var(--spectrum-gray-400);
--deselected-border-color-pressed: var(--spectrum-gray-500);
--deselected-background-color: var(--spectrum-gray-50);
--deselected-background-color-pressed: var(--spectrum-gray-100);
--selected-color: var(--spectrum-gray-800);
--selected-color-pressed: var(--spectrum-gray-900);
--text-color: var(--spectrum-alias-text-color);
--text-color-selected: var(--spectrum-gray-50);
color: var(--text-color);
background: var(--deselected-background-color);
border: 1px solid var(--deselected-border-color);
border-radius: 4px;
appearance: none;
vertical-align: middle;
font-size: 1rem;
text-align: center;
margin: 8px 0 0 0;
outline: none;
padding: 4px 12px;
&[data-pressed] {
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--deselected-background-color-pressed);
border-color: var(--deselected-border-color-pressed);
}
&[aria-pressed=true] {
background: var(--selected-color);
border-color: var(--selected-color);
color: var(--text-color-selected);
&[data-pressed] {
background: var(--selected-color-pressed);
border-color: var(--selected-color-pressed);
}
}
&[data-focus-visible] {
border-color: var(--selected-color);
box-shadow: 0 0 0 1px var(--selected-color);
}
}
@media (forced-colors: active) {
.react-aria-ToggleButton {
forced-color-adjust: none;
--deselected-border-color: ButtonBorder;
--deselected-border-color-pressed: ButtonBorder;
--deselected-background-color: ButtonFace;
--deselected-background-color-pressed: ButtonFace;
--selected-color: Highlight;
--selected-color-pressed: Highlight;
--text-color: ButtonText;
--text-color-selected: HighlightText;
}
}
Custom children#
TagGroup passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components.
Component | Context | Props | Ref |
Label | LabelContext | LabelProps | HTMLLabelElement |
Button | ButtonContext | ButtonProps | HTMLButtonElement |
Text | TextContext | TextProps | HTMLElement |
This example consumes from LabelContext
in an existing styled label component to make it compatible with React Aria Components. The useContextProps
hook merges the local props and ref with the ones provided via context by TagGroup.
import type {LabelProps} from 'react-aria-components';
import {LabelContext, useContextProps} from 'react-aria-components';
const MyCustomLabel = React.forwardRef(
(props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => {
// Merge the local props and ref with the ones provided via context.
[props, ref] = useContextProps(props, ref, LabelContext);
// ... your existing Label component
return <label {...props} ref={ref} />;
}
);
import type {LabelProps} from 'react-aria-components';
import {
LabelContext,
useContextProps
} from 'react-aria-components';
const MyCustomLabel = React.forwardRef(
(
props: LabelProps,
ref: React.ForwardedRef<HTMLLabelElement>
) => {
// Merge the local props and ref with the ones provided via context.
[props, ref] = useContextProps(
props,
ref,
LabelContext
);
// ... your existing Label component
return <label {...props} ref={ref} />;
}
);
import type {LabelProps} from 'react-aria-components';
import {
LabelContext,
useContextProps
} from 'react-aria-components';
const MyCustomLabel =
React.forwardRef(
(
props: LabelProps,
ref:
React.ForwardedRef<
HTMLLabelElement
>
) => {
// Merge the local props and ref with the ones provided via context.
[props, ref] =
useContextProps(
props,
ref,
LabelContext
);
// ... your existing Label component
return (
<label
{...props}
ref={ref}
/>
);
}
);
Now you can use MyCustomLabel
within a TagGroup
, in place of the builtin React Aria Components Label
.
<TagGroup>
<MyCustomLabel>Name</MyCustomLabel> {/* ... */}
</TagGroup>
<TagGroup>
<MyCustomLabel>Name</MyCustomLabel> {/* ... */}
</TagGroup>
<TagGroup>
<MyCustomLabel>
Name
</MyCustomLabel> {/* ... */}
</TagGroup>
State#
TagGroup provides a ListState
object to its children via ListStateContext
. This can be used to access and manipulate the TagGroup's state.
This example shows a SelectionCount
component that can be placed within a TagGroup
to display the number of selected tags.
import {ListStateContext} from 'react-aria-components';
function SelectionCount() {
let state = React.useContext(ListStateContext)!; let selected = state.selectionManager.selectedKeys.size;
return <small>{selected} tags selected.</small>;
}
<TagGroup selectionMode="multiple">
<Label>Tags</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<SelectionCount />
</TagGroup>
import {ListStateContext} from 'react-aria-components';
function SelectionCount() {
let state = React.useContext(ListStateContext)!; let selected = state.selectionManager.selectedKeys.size;
return <small>{selected} tags selected.</small>;
}
<TagGroup selectionMode="multiple">
<Label>Tags</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<SelectionCount />
</TagGroup>
import {ListStateContext} from 'react-aria-components';
function SelectionCount() {
let state = React
.useContext(
ListStateContext
)!; let selected =
state
.selectionManager
.selectedKeys.size;
return (
<small>
{selected}{' '}
tags selected.
</small>
);
}
<TagGroup selectionMode="multiple">
<Label>Tags</Label>
<TagList>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagList>
<SelectionCount />
</TagGroup>
Hooks#
If you need to customize things even further, such as accessing internal state, intercepting events, or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTagGroup for more details.