TagGroup
A wrapping set of tags you can turn on, and take off.
Overview
A wrapping set of tags you can turn on, and take off.
First example
<TagGroup selectionMode="multiple" defaultSelectedKeys={['fr']}><TagGroup.List><TagGroup.Item id="fr">Français</TagGroup.Item></TagGroup.List></TagGroup>
Anatomy
<TagGroup>
<TagGroup.List>
<TagGroup.Item id="…">
<TagGroup.ItemLabel>…</TagGroup.ItemLabel>
<TagGroup.ItemRemoveButton />
</TagGroup.Item>
</TagGroup.List>
</TagGroup>
TagGroup— what is selected, what is disabled, and every slot's resolved style.TagGroup.List— the wrapping row.TagGroup.Item— one tag.TagGroup.ItemLabel— its text.TagGroup.ItemRemoveButton— takes it off.
Usage
Selectable
<TagGroup selectionMode="multiple" defaultSelectedKeys={['fr']}>
<TagGroup.List>
<TagGroup.Item id="fr">Français</TagGroup.Item>
</TagGroup.List>
</TagGroup>
A stringifiable child becomes the label (R3). selectionMode is 'none' by default: a
group of tags is a set of labels until you say otherwise.
Removable
<TagGroup onRemove={id => setTags(t => t.filter(x => x !== id))}>
<TagGroup.Item id="fr">
<TagGroup.ItemLabel>Français</TagGroup.ItemLabel>
<TagGroup.ItemRemoveButton accessibilityLabel="Retirer Français" />
</TagGroup.Item>
</TagGroup>
The cross renders nothing without an onRemove. Removing a tag is your list changing,
and a cross that appeared to work while the list stayed put would be worse than one that is
plainly not there.
It is also written out rather than drawn by the item, because a tag you can turn on and a tag you can take off are different controls and most groups are only one of the two.
Style as props
<TagGroup.List gap={12} />
<TagGroup.Item paddingHorizontal={16} />
<TagGroup.ItemLabel fontSize={15} />
Full RN names, full RN values (R14). Every node takes them.
Root props
TagGroupProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| variant | TagGroupVariant | undefined | No | — |
| size | TagGroupSize | undefined | No | — |
| radius | RadiusKey | undefined | No | — |
| color | string | undefined | No | The tint (R7) — a raw value, never a token. Paints a selected tag. |
| selectionMode | TagSelectionMode | undefined | No | — |
| selectedKeys | readonly string[] | undefined | No | — |
| defaultSelectedKeys | readonly string[] | undefined | No | — |
| onSelectionChange | ((keys: readonly string[]) => void) | undefined | No | — |
| onRemove | ((id: string) => void) | undefined | No | Present is what makes a `TagGroup.ItemRemoveButton` do anything. |
| disabledKeys | readonly string[] | undefined | No | Ids that take no press. The group's own `isDisabled` covers all of them. |
| isDisabled | boolean | undefined | No | — |
| isDeselectable | boolean | undefined | No | Whether pressing the selected tag clears it. Off, a `single` group always has one. |
Slots
TagGroupItemLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
TagGroupItemProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | — |
| isDisabled | boolean | undefined | No | — |
| children | ReactNode | ((state: TagItemRenderState) => ReactNode) | No | — |
| asChild | boolean | undefined | No | — |
TagGroupItemRemoveButtonProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| asChild | boolean | undefined | No | — |
| isDisabled | boolean | undefined | No | An explicit value wins over the tag's own. |
TagGroupListProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
Accessibility
The list is a list, each tag a button carrying selected and disabled. The cross
takes the tag's own isDisabled — a disabled tag that can still be removed is not disabled
— and warns in development when it has no accessibilityLabel, because a cross announces
as nothing on its own.
Migration from legacy
Implementation notes
This is not a row of `Chip`s
A chip is a piece of metadata that is always the same. A tag is one you can turn on, take off, or both.
The selection state and the removal are the component; the pill around them is the least of it. That is also why the two do not share a recipe — a chip has ten variants because it reports an intent, and a tag has two grounds because it reports nothing at all until it is selected.
Two grounds, not two emphases
default is the theme's neutral fill; surface is the card colour. They swap so a tag
never disappears into what is behind it — a group on a card wants default, a group on the
page wants surface.
A selected tag leaves both and takes the accent's soft slice, which is the only place this component uses colour.
Wrapping is the point
A tag group is a set of the same kind of thing, and a set that scrolls sideways hides how many of it there are — which is the one fact a reader wants from a row of tags.
The selection rule
One pure function, and it returns the list unchanged whenever a press changes nothing:
'none' refuses every press, 'single' on the already-selected tag clears it unless that
would empty a group the caller asked to keep one. useControllableState drops a set to the
value it already holds, so onSelectionChange never fires for a change that did not happen.
Ten tests cover it.