Skip to main content

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
Please 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: :hover or [data-hovered="true"] on tag
  • Focus: :focus-visible or [data-focus-visible="true"] on tag
  • Pressed: :active or [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

PropTypeDefaultDescription
selectionMode"none" | "single" | "multiple""none"The type of selection that is allowed
selectedKeysIterable<string>-The currently selected keys (controlled)
defaultSelectedKeysIterable<string>-The initial selected keys (uncontrolled)
onSelectionChange(keys: Set<string>) => void-Handler called when the selection changes
disabledKeysIterable<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
classstring-Additional CSS classes
childrenJSX.Element-TagGroup content

TagGroup.List Props

PropTypeDefaultDescription
itemsreadonly T[]-The items to display in the tag list
renderEmptyState() => JSX.Element-Function to render when the list is empty
classstring-Additional CSS classes
childrenJSX.Element | (item) => JSX.Element-TagList content or per-item render function

Tag Props

PropTypeDefaultDescription
idstring-The unique identifier for the tag
textValuestring-A string representation of the tag's content
isDisabledboolean-Whether the tag is disabled
classstring-Additional CSS classes
childrenJSX.Element | (renderProps) => JSX.Element-Tag content or render function

Note: size, variant are inherited from the parent TagGroup component.

Tag.RemoveButton Props

PropTypeDefaultDescription
classstring-Additional CSS classes
childrenJSX.Element-Custom remove button content (defaults to close icon)

When onRemove is provided to TagGroup:

  • Auto-rendering: If no custom Tag.RemoveButton is included in the Tag children, a default remove button is automatically rendered.
  • Custom icon: You can pass custom content (like icons) to Tag.RemoveButton children to customize the appearance.

Differences from HeroUI React

  • Use class instead of className.
  • Built from scratch on Solid primitives (React Aria's TagGroup/TagList/Tag have 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, and isDisabled (React Aria's isHovered/isPressed/isFocusVisible are handled by CSS pseudo-classes instead).

Last updated: 7/19/26, 3:27 AM

HeroUI SolidUnofficial SolidJS port of HeroUI v3, built on Kobalte and @heroui/styles