alpha

TagGroup

A tag group is a focusable list of labels, categories, keywords, filters, or other items, with support for keyboard navigation, selection, and removal.

installyarn add react-aria-components
version1.0.0-alpha.6
usageimport {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;
    }

    &[aria-selected=true] {
      border-color: var(--background-selected);
      background: var(--background-selected);
      color: var(--text-color-selected);
    }

    &[aria-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[aria-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;
    }

    &[aria-selected=true] {
      border-color: var(--background-selected);
      background: var(--background-selected);
      color: var(--text-color-selected);
    }

    &[aria-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[aria-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;
    }

    &[aria-selected=true] {
      border-color: var(--background-selected);
      background: var(--background-selected);
      color: var(--text-color-selected);
    }

    &[aria-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[aria-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 and replace 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#


TravelingGrid rowGrid cellTag labelHikingRemove buttonTag groupCategoriesGroup label

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.

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.

Label
A label provides context for an element.
Button
A button allows a user to perform an action.

Props#


TagGroup#

NameTypeDefaultDescription
selectionBehaviorSelectionBehaviorHow multiple selection should behave in the collection.
disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionModeSelectionModeThe type of selection that is allowed in the collection.
disallowEmptySelectionbooleanWhether 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).
childrenReactNodeThe children of the component.
classNamestringThe CSS className for the element.
styleCSSPropertiesThe inline style for the element.
Events
NameTypeDefaultDescription
onRemove( (keys: Set<Key> )) => voidHandler that is called when a user deletes a tag.
onSelectionChange( (keys: Selection )) => anyHandler that is called when the selection changes.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

TagList#

A <TagList> is a list of tags within a <TagGroup>.

NameTypeDefaultDescription
renderEmptyState() => ReactNodeProvides content to display when there are no items in the tag list.
childrenReactNode( (item: T )) => ReactNodeThe contents of the collection.
itemsIterable<T>Item objects in the collection.
classNamestring( (values: TagListRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TagListRenderProps )) => CSSPropertiesThe 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.

NameTypeDefaultDescription
textValuestring

A string representation of the tags's contents, used for accessibility. Required if children is not a plain text string.

childrenReactNode( (values: TagRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: TagRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TagRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Accessibility
NameTypeDefaultDescription
idKeyA 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 DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. For example:

.react-aria-Tag[aria-selected=true] {
  /* ... */
}

.react-aria-Tag[data-focused] {
  /* ... */
}
.react-aria-Tag[aria-selected=true] {
  /* ... */
}

.react-aria-Tag[data-focused] {
  /* ... */
}
.react-aria-Tag[aria-selected=true] {
  /* ... */
}

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

NameCSS SelectorDescription
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.

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:

NameCSS SelectorDescription
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[aria-selected=true]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[aria-disabled]

Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on disabledKeys and disabledBehavior.

selectionModeThe type of selection that is allowed in the collection.
selectionBehaviorThe 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.

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>

Usage#


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

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>

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>

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

Hooks#

If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTagGroup for more details.