A focusable list of tags with support for keyboard navigation, selection, and removal
Import
import { TagGroup, Tag } from "heroui-solid";Usage
News
Travel
Gaming
Shopping
import {
PlanetEarth,
Rocket,
ShoppingBag,
SquareArticle
} from "gravity-icons-solid"
import { Tag, TagGroup } from "heroui-solid"
export function TagGroupBasic() {
return (
<TagGroup aria-label="Tags" selectionMode="single">
<TagGroup.List>
<Tag id="default-news">
<SquareArticle />
News
</Tag>
<Tag id="default-travel">
<PlanetEarth />
Travel
</Tag>
<Tag id="default-gaming">
<Rocket />
Gaming
</Tag>
<Tag id="default-shopping">
<ShoppingBag />
Shopping
</Tag>
</TagGroup.List>
</TagGroup>
)
}Anatomy
import { TagGroup, Tag, Label, Description, ErrorMessage } from "heroui-solid";
export default () => (
<TagGroup>
<Label />
<TagGroup.List>
<Tag>
<Tag.RemoveButton />
</Tag>
</TagGroup.List>
<Description />
<ErrorMessage />
</TagGroup>
);Sizes
News
Travel
Gaming
News
Travel
Gaming
News
Travel
Gaming
import { Label, Tag, TagGroup } from "heroui-solid"
export function TagGroupSizes() {
return (
<div class="flex flex-col gap-6">
<TagGroup selectionMode="single" size="sm">
<Label>Small</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
</TagGroup.List>
</TagGroup>
<TagGroup selectionMode="single" size="md">
<Label>Medium</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
</TagGroup.List>
</TagGroup>
<TagGroup selectionMode="single" size="lg">
<Label>Large</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
</TagGroup.List>
</TagGroup>
</div>
)
}Variants
News
Travel
Gaming
News
Travel
Gaming
import { Label, Tag, TagGroup } from "heroui-solid"
export function TagGroupVariants() {
return (
<div class="flex flex-col gap-8">
<TagGroup selectionMode="single" variant="default">
<Label>Default</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
</TagGroup.List>
</TagGroup>
<TagGroup selectionMode="single" variant="surface">
<Label>Surface</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
</TagGroup.List>
</TagGroup>
</div>
)
}Disabled
News
Travel
Gaming
Some tags are disabled
News
Travel
Gaming
Tags disabled via disabledKeys prop
import { Description, Label, Tag, TagGroup } from "heroui-solid"
export function TagGroupDisabled() {
return (
<div class="flex flex-col gap-4">
<TagGroup selectionMode="single">
<Label>Disabled Tags</Label>
<TagGroup.List>
<Tag isDisabled>News</Tag>
<Tag>Travel</Tag>
<Tag isDisabled>Gaming</Tag>
</TagGroup.List>
<Description>Some tags are disabled</Description>
</TagGroup>
<TagGroup disabledKeys={["travel"]} selectionMode="single">
<Label>Disabled Keys</Label>
<TagGroup.List>
<Tag id="news">News</Tag>
<Tag id="travel">Travel</Tag>
<Tag id="gaming">Gaming</Tag>
</TagGroup.List>
<Description>Tags disabled via disabledKeys prop</Description>
</TagGroup>
</div>
)
}Selection Modes
News
Travel
Gaming
Shopping
Choose one category
News
Travel
Gaming
Shopping
Choose multiple categories
import { Description, Label, Tag, TagGroup } from "heroui-solid"
import { createSignal } from "solid-js"
export function TagGroupSelectionModes() {
const [singleSelected, setSingleSelected] = createSignal<Set<string>>(
new Set(["news"])
)
const [multipleSelected, setMultipleSelected] = createSignal<Set<string>>(
new Set(["news", "travel"])
)
return (
<div class="flex flex-col gap-8">
<TagGroup
selectedKeys={singleSelected()}
selectionMode="single"
onSelectionChange={(keys) => setSingleSelected(keys)}
>
<Label>Single Selection</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagGroup.List>
<Description>Choose one category</Description>
</TagGroup>
<TagGroup
selectedKeys={multipleSelected()}
selectionMode="multiple"
onSelectionChange={(keys) => setMultipleSelected(keys)}
>
<Label>Multiple Selection</Label>
<TagGroup.List>
<Tag>News</Tag>
<Tag>Travel</Tag>
<Tag>Gaming</Tag>
<Tag>Shopping</Tag>
</TagGroup.List>
<Description>Choose multiple categories</Description>
</TagGroup>
</div>
)
}Controlled
News
Travel
Gaming
Shopping
Selected: news, travel
import { Description, Label, Tag, TagGroup } from "heroui-solid"
import { createSignal } from "solid-js"
export function TagGroupControlled() {
const [selected, setSelected] = createSignal<Set<string>>(
new Set(["news", "travel"])
)
return (
<div class="flex flex-col gap-3">
<TagGroup
selectedKeys={selected()}
selectionMode="multiple"
onSelectionChange={(keys) => setSelected(keys)}
>
<Label>Categories (controlled)</Label>
<TagGroup.List>
<Tag id="news">News</Tag>
<Tag id="travel">Travel</Tag>
<Tag id="gaming">Gaming</Tag>
<Tag id="shopping">Shopping</Tag>
</TagGroup.List>
<Description>
Selected:{" "}
{Array.from(selected()).length > 0
? Array.from(selected()).join(", ")
: "None"}
</Description>
</TagGroup>
</div>
)
}With Error Message
Laundry
Fitness center
Parking
Swimming pool
Breakfast
Select at least one category
import { Description, ErrorMessage, Label, Tag, TagGroup } from "heroui-solid"
import { createSignal } from "solid-js"
export function TagGroupWithErrorMessage() {
const [selected, setSelected] = createSignal<Set<string>>(new Set())
const isInvalid = () => Array.from(selected()).length === 0
return (
<TagGroup
selectedKeys={selected()}
selectionMode="multiple"
onSelectionChange={(keys) => setSelected(keys)}
>
<Label>Amenities</Label>
<TagGroup.List>
<Tag id="laundry">Laundry</Tag>
<Tag id="fitness">Fitness center</Tag>
<Tag id="parking">Parking</Tag>
<Tag id="pool">Swimming pool</Tag>
<Tag id="breakfast">Breakfast</Tag>
</TagGroup.List>
<Description>
{isInvalid()
? "Select at least one category"
: `Selected: ${Array.from(selected()).join(", ")}`}
</Description>
<ErrorMessage>
{isInvalid() && <>Please select at least one category</>}
</ErrorMessage>
</TagGroup>
)
}With Prefix
News
Travel
Gaming
Shopping
Tags with icons
FFred
MMichael
JJane
Tags with avatars
import {
PlanetEarth,
Rocket,
ShoppingBag,
SquareArticle
} from "gravity-icons-solid"
import { Avatar, Description, Label, Tag, TagGroup } from "heroui-solid"
export function TagGroupWithPrefix() {
return (
<div class="flex flex-col gap-8">
<TagGroup selectionMode="single">
<Label>With Icons</Label>
<TagGroup.List>
<Tag>
<SquareArticle />
News
</Tag>
<Tag>
<PlanetEarth />
Travel
</Tag>
<Tag>
<Rocket />
Gaming
</Tag>
<Tag>
<ShoppingBag />
Shopping
</Tag>
</TagGroup.List>
<Description>Tags with icons</Description>
</TagGroup>
<TagGroup selectionMode="single">
<Label>With Avatars</Label>
<TagGroup.List>
<Tag>
<Avatar class="size-4">
<Avatar.Image src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg" />
<Avatar.Fallback>F</Avatar.Fallback>
</Avatar>
Fred
</Tag>
<Tag>
<Avatar class="size-4">
<Avatar.Image src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg" />
<Avatar.Fallback>M</Avatar.Fallback>
</Avatar>
Michael
</Tag>
<Tag>
<Avatar class="size-4">
<Avatar.Image src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg" />
<Avatar.Fallback>J</Avatar.Fallback>
</Avatar>
Jane
</Tag>
</TagGroup.List>
<Description>Tags with avatars</Description>
</TagGroup>
</div>
)
}With Remove Button
News
Travel
Gaming
Shopping
Click the X to remove tags
React
Vue
Angular
Svelte
Custom remove button with icon
import { CircleXmarkFill } from "gravity-icons-solid"
import { Description, EmptyState, Label, Tag, TagGroup } from "heroui-solid"
import { createSignal } from "solid-js"
type TagItem = { id: string; name: string }
export function TagGroupWithRemoveButton() {
const [tags, setTags] = createSignal<TagItem[]>([
{ id: "news", name: "News" },
{ id: "travel", name: "Travel" },
{ id: "gaming", name: "Gaming" },
{ id: "shopping", name: "Shopping" }
])
const [frameworks, setFrameworks] = createSignal<TagItem[]>([
{ id: "react", name: "React" },
{ id: "vue", name: "Vue" },
{ id: "angular", name: "Angular" },
{ id: "svelte", name: "Svelte" }
])
const onRemoveTags = (keys: Set<string>) => {
setTags(tags().filter((tag) => !keys.has(tag.id)))
}
const onRemoveFrameworks = (keys: Set<string>) => {
setFrameworks(frameworks().filter((framework) => !keys.has(framework.id)))
}
return (
<div class="flex flex-col gap-8">
<div class="w-sm">
<TagGroup selectionMode="single" onRemove={onRemoveTags}>
<Label>Default Remove Button</Label>
<TagGroup.List
items={tags()}
renderEmptyState={() => (
<EmptyState class="p-1">No categories found</EmptyState>
)}
>
{(tag) => (
<Tag id={tag.id} textValue={tag.name}>
{tag.name}
</Tag>
)}
</TagGroup.List>
<Description>Click the X to remove tags</Description>
</TagGroup>
</div>
<div class="w-md">
<TagGroup selectionMode="single" onRemove={onRemoveFrameworks}>
<Label>Custom Remove Button</Label>
<TagGroup.List
items={frameworks()}
renderEmptyState={() => (
<EmptyState class="p-1">No frameworks found</EmptyState>
)}
>
{(tag) => (
<Tag id={tag.id} textValue={tag.name}>
{(renderProps) => (
<>
{tag.name}
{renderProps.allowsRemoving && (
<Tag.RemoveButton>
<CircleXmarkFill />
</Tag.RemoveButton>
)}
</>
)}
</Tag>
)}
</TagGroup.List>
<Description>Custom remove button with icon</Description>
</TagGroup>
</div>
</div>
)
}Styling
Passing Tailwind CSS classes
import { TagGroup, Tag, Label } from "heroui-solid";
function CustomTagGroup() {
return (
<TagGroup class="w-full">
<Label>Categories</Label>
<TagGroup.List class="gap-2">
<Tag class="rounded-lg px-4 py-2 font-bold">Custom Styled</Tag>
</TagGroup.List>
</TagGroup>
);
}Customizing the component classes
To customize the TagGroup component classes, you can use the @layer components directive.
@layer components {
.tag-group {
@apply flex flex-col gap-2;
}
.tag-group__list {
@apply flex flex-wrap gap-2;
}
.tag {
@apply rounded-full px-3 py-1;
}
.tag__remove-button {
@apply ml-1;
}
}CSS Classes
Base Classes
.tag-group- Base tag group container.tag-group__list- Container for the list of tags.tag- Base tag styles.tag__remove-button- Remove button trigger
Slot Classes
.tag-group [slot="description"]- Description slot styles.tag-group [slot="errorMessage"]- ErrorMessage slot styles
Size Classes
.tag--sm- Small size tag.tag--md- Medium size tag (default).tag--lg- Large size tag
Variant Classes
.tag--default- Default variant.tag--surface- Surface variant with surface background
State Classes
.tag[data-selected="true"]- Selected tag state.tag[data-disabled="true"]- Disabled tag state.tag[data-hovered="true"]- Hovered tag state.tag[data-focus-visible="true"]- Focused tag state (keyboard focus)
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveror[data-hovered="true"]on tag - Focus:
:focus-visibleor[data-focus-visible="true"]on tag - Pressed:
:activeor[data-pressed="true"]on tag - Selected:
[data-selected="true"]or[aria-selected="true"]on tag - Disabled:
[data-disabled="true"]on tag
API Reference
TagGroup Props
| Prop | Type | Default | Description |
|---|---|---|---|
selectionMode | "none" | "single" | "multiple" | "none" | The type of selection that is allowed |
selectedKeys | Iterable<string> | - | The currently selected keys (controlled) |
defaultSelectedKeys | Iterable<string> | - | The initial selected keys (uncontrolled) |
onSelectionChange | (keys: Set<string>) => void | - | Handler called when the selection changes |
disabledKeys | Iterable<string> | - | Keys of disabled tags |
onRemove | (keys: Set<string>) => void | - | Handler called when tags are removed |
size | "sm" | "md" | "lg" | "md" | Size of the tags in the group |
variant | "default" | "surface" | "default" | Visual variant of the tags |
class | string | - | Additional CSS classes |
children | JSX.Element | - | TagGroup content |
TagGroup.List Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | readonly T[] | - | The items to display in the tag list |
renderEmptyState | () => JSX.Element | - | Function to render when the list is empty |
class | string | - | Additional CSS classes |
children | JSX.Element | (item) => JSX.Element | - | TagList content or per-item render function |
Tag Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | The unique identifier for the tag |
textValue | string | - | A string representation of the tag's content |
isDisabled | boolean | - | Whether the tag is disabled |
class | string | - | Additional CSS classes |
children | JSX.Element | (renderProps) => JSX.Element | - | Tag content or render function |
Note: size, variant are inherited from the parent TagGroup component.
Tag.RemoveButton Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Custom remove button content (defaults to close icon) |
When onRemove is provided to TagGroup:
- Auto-rendering: If no custom
Tag.RemoveButtonis included in theTagchildren, a default remove button is automatically rendered. - Custom icon: You can pass custom content (like icons) to
Tag.RemoveButtonchildren to customize the appearance.
Differences from HeroUI React
- Use
classinstead ofclassName. - Built from scratch on Solid primitives (React Aria's
TagGroup/TagList/Taghave no Kobalte equivalent): selection,onRemove, disabled keys, and roving-tabindex keyboard navigation (arrows, Home/End, Delete/Backspace) are provided, adapting React Aria's grid interaction model. - The per-item render function exposes
allowsRemoving,isSelected, andisDisabled(React Aria'sisHovered/isPressed/isFocusVisibleare handled by CSS pseudo-classes instead).
Last updated: 7/19/26, 3:27 AM