TagGroup
Removable tags
Use arrow keys to move between tags and Delete or Backspace to remove one. The supplied-source tag is disabled.
Supply unique item IDs and keep the collection in application state. onRemove requests a removal by ID. The item remains visible until the parent supplies the changed items.
import { useRef, useState } from "react";
import { Button, TagGroup, type TagItem } from "@operator-ds/react";
function ActiveFilters() {
const [items, setItems] = useState<TagItem[]>([{ id: "name", label: "Name contains Alpha" }]);
const addRef = useRef<HTMLButtonElement>(null);
return (
<>
<Button ref={addRef} onClick={() => setItems([{ id: "name", label: "Name contains Alpha" }])}>
Add example filter
</Button>
<TagGroup
label="Active filters"
items={items}
onRemove={(id) => setItems((current) => current.filter((item) => item.id !== id))}
emptyMessage="No active filters"
returnFocusRef={addRef}
/>
</>
);
}
returnFocusRef receives focus when the application removes the last tag while focus is in the group, on the next render or after a server round trip. A declined removal keeps the item and its focus, and an operator who has moved on keeps their focus when the removal is accepted later. Without onRemove, tags display their labels without remove actions.
Native attributes pass through. id, data-*, focus, blur and key handlers, title, tabIndex, role and every other div attribute reach the group element; aria-labelledby, aria-describedby and aria-details go to the tag list, which they name and describe. Arrow keys are handled inside the list, so a group onKeyDown does not see the ones React Aria uses for moving between tags. Two are not applied: aria-label, because label names the tag list, and dangerouslySetInnerHTML, because the tags come from items; in development a structured PROP_NOT_APPLIED error says so.
Display and disabled states
Use an item's disabled flag to block its removal, or disable the entire collection. Labels preserve authored case and wrap within the available width.
Props
| PROP | TYPE | DEFAULT | REQUIRED |
|---|---|---|---|
itemsReadonly TagItem[]: unique string id, label and optional disabled. | array | — | yes |
labelAccessible name for the tag collection. | string | — | yes |
onRemoveRequests removal by id. The parent supplies the updated items; declined removals retain the item and focus. | function | — | no |
disabled | boolean | false | no |
emptyMessageOptional message when items is empty. | string | — | no |
returnFocusRefRefObject<HTMLElement | null> focused when the application removes the last tag while focus is in the group, on the next render or after a server round trip. An operator who has moved on keeps their focus. | node | — | no |
className | string | — | no |
Accessibility
- React Aria supplies the labeled tag grid, arrow navigation, and Delete/Backspace behavior.
- Remove actions include the full tag label in their accessible name.
- Accepted removal moves focus to a remaining tag; the final removal can return it to a supplied control.
- Disabled items cannot request removal. Focus indicators and remove targets use OPERATOR tokens.
Manifest entry
<TagGroup label="Active filters" items={filters} onRemove={(id) => setFilters(filters.filter((item) => item.id !== id))} returnFocusRef={addButtonRef} />View agent manifest
{
"description": "A labeled collection of tags with optional controlled removal, native keyboard navigation and authored label casing.",
"props": {
"items": {
"type": "array",
"required": true,
"description": "Readonly TagItem[]: unique string id, label and optional disabled."
},
"label": {
"type": "string",
"required": true,
"description": "Accessible name for the tag collection."
},
"onRemove": {
"type": "function",
"description": "Requests removal by id. The parent supplies the updated items; declined removals retain the item and focus."
},
"disabled": {
"type": "boolean",
"default": false
},
"emptyMessage": {
"type": "string",
"description": "Optional message when items is empty."
},
"returnFocusRef": {
"type": "node",
"description": "RefObject<HTMLElement | null> focused when the application removes the last tag while focus is in the group, on the next render or after a server round trip. An operator who has moved on keeps their focus."
},
"className": {
"type": "string"
}
},
"composable_with": [
"PropertyFilter"
],
"never_inside": [
"Button"
],
"example": "<TagGroup label=\"Active filters\" items={filters} onRemove={(id) => setFilters(filters.filter((item) => item.id !== id))} returnFocusRef={addButtonRef} />"
}