Skip to content
Lenso UI

TagGroup 标签组

可聚焦的标签列表,支持键盘导航、选择与移除。

用法

import { TagGroup } from '@lenso/ui';

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";
// Adapted from HeroUI v3.2.6 tag-group-basic (Apache-2.0).import { PlanetEarth, Rocket, ShoppingBag, SquareArticle } from "@gravity-ui/icons";import { Tag, TagGroup } from "@lenso/ui";
export function TagGroupBasic() {  return (    <TagGroup aria-label="Tags" selectionMode="single">      <TagGroup.List>        <Tag itemKey="default-news" textValue="News">          <SquareArticle width={12} height={12} />          News        </Tag>        <Tag itemKey="default-travel" textValue="Travel">          <PlanetEarth width={12} height={12} />          Travel        </Tag>        <Tag itemKey="default-gaming" textValue="Gaming">          <Rocket width={12} height={12} />          Gaming        </Tag>        <Tag itemKey="default-shopping" textValue="Shopping">          <ShoppingBag width={12} height={12} />          Shopping        </Tag>      </TagGroup.List>    </TagGroup>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

组件结构

import { TagGroup, Tag, Label, Description, ErrorMessage } from '@lenso/ui';
export default () => (  <TagGroup>    <Label />    <TagGroup.List>      <Tag>        <Tag.RemoveButton />      </Tag>    </TagGroup.List>    <Description />    <ErrorMessage />  </TagGroup>)

示例

尺寸

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 sizes adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { TagGroup } from "@lenso/ui";import { Label } from "./text";import { Categories, styles } from "./categories";export function TagGroupSizes() {  return (    <div {...stylex.props(styles.sizes)}>      {(["sm", "md", "lg"] as const).map((size, index) => (        <TagGroup          key={size}          aria-label={["Small", "Medium", "Large"][index]}          selectionMode="single"          size={size}        >          <Label>{["Small", "Medium", "Large"][index]}</Label>          <TagGroup.List>            <Categories count={3} />          </TagGroup.List>        </TagGroup>      ))}    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

变体

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 variants adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { TagGroup } from "@lenso/ui";import { Label } from "./text";import { Categories, styles } from "./categories";export function TagGroupVariants() {  return (    <div {...stylex.props(styles.stack)}>      {(["default", "surface"] as const).map((variant) => (        <TagGroup          key={variant}          aria-label={variant === "default" ? "Default" : "Surface"}          selectionMode="single"          variant={variant}        >          <Label>{variant === "default" ? "Default" : "Surface"}</Label>          <TagGroup.List>            <Categories count={3} />          </TagGroup.List>        </TagGroup>      ))}    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

禁用

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 disabled adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { TagGroup } from "@lenso/ui";import { Description, Label } from "./text";import { Categories, styles } from "./categories";export function TagGroupDisabled() {  return (    <div {...stylex.props(styles.disabled)}>      <TagGroup aria-label="Disabled Tags" selectionMode="single">        <Label>Disabled Tags</Label>        <TagGroup.List>          <Categories disabled count={3} />        </TagGroup.List>        <Description>Some tags are disabled</Description>      </TagGroup>      <TagGroup        aria-label="Disabled Keys"        disabledKeys={new Set(["travel"])}        selectionMode="single"      >        <Label>Disabled Keys</Label>        <TagGroup.List>          <Categories count={3} />        </TagGroup.List>        <Description>Tags disabled via disabledKeys prop</Description>      </TagGroup>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

选择模式

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 selection-modes adaptation (Apache-2.0).import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { TagGroup } from "@lenso/ui";import { Description, Label } from "./text";import { Categories, styles } from "./categories";export function TagGroupSelectionModes() {  const [single, setSingle] = useState<Set<Key>>(new Set(["news"]));  const [multiple, setMultiple] = useState<Set<Key>>(new Set(["news", "travel"]));  return (    <div {...stylex.props(styles.stack)}>      <TagGroup        aria-label="Single Selection"        selectedKeys={single}        selectionMode="single"        onSelectionChange={setSingle}      >        <Label>Single Selection</Label>        <TagGroup.List>          <Categories />        </TagGroup.List>        <Description>Choose one category</Description>      </TagGroup>      <TagGroup        aria-label="Multiple Selection"        selectedKeys={multiple}        selectionMode="multiple"        onSelectionChange={setMultiple}      >        <Label>Multiple Selection</Label>        <TagGroup.List>          <Categories />        </TagGroup.List>        <Description>Choose multiple categories</Description>      </TagGroup>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

受控组件

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 controlled adaptation (Apache-2.0).import { useState, type Key } from "react";import { TagGroup } from "@lenso/ui";import { Description, Label } from "./text";import { Categories } from "./categories";export function TagGroupControlled() {  const [selected, setSelected] = useState<Set<Key>>(new Set(["news", "travel"]));  return (    <TagGroup      aria-label="Categories (controlled)"      selectedKeys={selected}      selectionMode="multiple"      onSelectionChange={setSelected}    >      <Label>Categories (controlled)</Label>      <TagGroup.List>        <Categories />      </TagGroup.List>      <Description>Selected: {selected.size ? [...selected].join(", ") : "None"}</Description>    </TagGroup>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

带错误信息

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 with-error-message adaptation (Apache-2.0).import { useState, type Key } from "react";import { Tag, TagGroup } from "@lenso/ui";import { Description, ErrorMessage, Label } from "./text";const amenities = [  ["laundry", "Laundry"],  ["fitness", "Fitness center"],  ["parking", "Parking"],  ["pool", "Swimming pool"],  ["breakfast", "Breakfast"],];export function TagGroupWithErrorMessage() {  const [selected, setSelected] = useState<Set<Key>>(new Set());  return (    <TagGroup      aria-label="Amenities"      selectedKeys={selected}      selectionMode="multiple"      onSelectionChange={setSelected}      aria-invalid={!selected.size}    >      <Label>Amenities</Label>      <TagGroup.List>        {amenities.map(([key, name]) => (          <Tag key={key} itemKey={key!} textValue={name!}>            {name}          </Tag>        ))}      </TagGroup.List>      <Description>        {!selected.size ? "Select at least one category" : `Selected: ${[...selected].join(", ")}`}      </Description>      <ErrorMessage>{!selected.size && "Please select at least one category"}</ErrorMessage>    </TagGroup>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

带列表数据

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 with-list-data adaptation (Apache-2.0); native controlled collection.import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { Avatar, EmptyState, Tag, TagGroup } from "@lenso/ui";import { Description, Label } from "./text";import { styles } from "./categories";const initial = ["Fred", "Michael", "Jane", "Alice", "Bob", "Charlie"].map((name, index) => ({  key: name.toLowerCase(),  textValue: name,  color: ["blue", "green", "purple", "red", "orange", "black"][index],}));export function TagGroupWithListData() {  const [items, setItems] = useState(initial);  const [selected, setSelected] = useState<Set<Key>>(new Set(["fred", "michael"]));  const avatar = (user: (typeof initial)[number]) => (    <Avatar xstyle={styles.avatar} size="sm">      <Avatar.Image        alt={user.textValue}        src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${user.color}.jpg`}      />      <Avatar.Fallback>{user.textValue[0]}</Avatar.Fallback>    </Avatar>  );  return (    <div {...stylex.props(styles.width)}>      <TagGroup        aria-label="Team Members"        selectedKeys={selected}        selectionMode="multiple"        onSelectionChange={setSelected}        onRemove={(keys) => setItems((previous) => previous.filter((user) => !keys.has(user.key)))}      >        <Label>Team Members</Label>        <TagGroup.List>          {items.length ? (            items.map((user) => (              <Tag key={user.key} itemKey={user.key} textValue={user.textValue}>                {avatar(user)}                {user.textValue}              </Tag>            ))          ) : (            <EmptyState>No team members</EmptyState>          )}        </TagGroup.List>        <Description>Select team members for your project</Description>      </TagGroup>      {selected.size > 0 && (        <>          <p>Selected:</p>          <div {...stylex.props(styles.selected)}>            {items              .filter((user) => selected.has(user.key))              .map((user) => (                <div key={user.key} {...stylex.props(styles.selectedUser)}>                  {avatar(user)}                  <span>{user.textValue}</span>                </div>              ))}          </div>        </>      )}    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

带前缀

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 with-prefix adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Avatar, Tag, TagGroup } from "@lenso/ui";import { Description, Label } from "./text";import { Categories, styles } from "./categories";export function TagGroupWithPrefix() {  return (    <div {...stylex.props(styles.stack)}>      <TagGroup aria-label="With Icons" selectionMode="single">        <Label>With Icons</Label>        <TagGroup.List>          <Categories icons />        </TagGroup.List>        <Description>Tags with icons</Description>      </TagGroup>      <TagGroup aria-label="With Avatars" selectionMode="single">        <Label>With Avatars</Label>        <TagGroup.List>          {["Fred", "Michael", "Jane"].map((name, index) => (            <Tag key={name} itemKey={name.toLowerCase()} textValue={name}>              <Avatar xstyle={styles.avatar}>                <Avatar.Image                  alt={name}                  src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${["blue", "green", "purple"][index]}.jpg`}                />                <Avatar.Fallback>{name[0]}</Avatar.Fallback>              </Avatar>              {name}            </Tag>          ))}        </TagGroup.List>        <Description>Tags with avatars</Description>      </TagGroup>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

带移除按钮

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 with-remove-button adaptation (Apache-2.0).import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { Icon } from "@iconify/react";import { EmptyState, Tag, TagGroup } from "@lenso/ui";import { Description, Label } from "./text";import { styles } from "./categories";function Removable({ custom = false }: { custom?: boolean }) {  const [items, setItems] = useState(    custom ? ["React", "Vue", "Angular", "Svelte"] : ["News", "Travel", "Gaming", "Shopping"],  );  const remove = (keys: Set<Key>) =>    setItems((previous) => previous.filter((name) => !keys.has(name.toLowerCase())));  return (    <TagGroup      aria-label={custom ? "Custom Remove Button" : "Default Remove Button"}      selectionMode="single"      onRemove={remove}    >      <Label>{custom ? "Custom Remove Button" : "Default Remove Button"}</Label>      <TagGroup.List>        {items.length ? (          items.map((name) => (            <Tag key={name} itemKey={name.toLowerCase()} textValue={name}>              {custom                ? ({ allowsRemoving }) => (                    <>                      {name}                      {allowsRemoving && (                        <Tag.RemoveButton>                          <Icon icon="gravity-ui:circle-xmark-fill" width={12} aria-hidden="true" />                        </Tag.RemoveButton>                      )}                    </>                  )                : name}            </Tag>          ))        ) : (          <EmptyState>{custom ? "No frameworks found" : "No categories found"}</EmptyState>        )}      </TagGroup.List>      <Description>        {custom ? "Custom remove button with icon" : "Click the X to remove tags"}      </Description>    </TagGroup>  );}export function TagGroupWithRemoveButton() {  return (    <div {...stylex.props(styles.stack)}>      <Removable />      <Removable custom />    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

渲染函数

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 render-function adaptation (Apache-2.0).import { TagGroup } from "@lenso/ui";import { Categories } from "./categories";export function RenderFunction() {  return (    <TagGroup      aria-label="Tags"      render={(props) => <div {...props} data-custom="foo" />}      selectionMode="single"    >      <TagGroup.List>        <Categories icons prefix="default-" />      </TagGroup.List>    </TagGroup>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

自定义样式

Tailwind CSS

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";// HeroUI v3.2.6 custom-styles adaptation (Apache-2.0).import { TagGroup } from "@lenso/ui";import { Categories, styles } from "./categories";export function CustomStyles() {  return (    <TagGroup aria-label="Topics" selectionMode="single">      <TagGroup.List xstyle={styles.list}>        <Categories icons custom />      </TagGroup.List>    </TagGroup>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

全局 CSS

若要自定义组件类,可使用 @layer components 指令。了解更多。

@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;  }}

样式参考

HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。

CSS 类

TagGroup 组件使用以下 CSS 类(查看源码样式 与 tag.css):

基础类 [!toc]

  • .tag-group - TagGroup 根容器
  • .tag-group__list - 标签列表容器
  • .tag - 标签基础样式
  • .tag__remove-button - 移除按钮触发器

插槽类 [!toc]

  • .tag-group [slot="description"] - Description 插槽样式
  • .tag-group [slot="errorMessage"] - ErrorMessage 插槽样式

尺寸类 [!toc]

  • .tag--sm - 小尺寸标签
  • .tag--md - 中尺寸标签(默认)
  • .tag--lg - 大尺寸标签

变体类 [!toc]

  • .tag--default - 默认变体
  • .tag--surface - 带 Surface 背景的变体

状态类 [!toc]

  • .tag[data-selected="true"] - 选中状态
  • .tag[data-disabled="true"] - 禁用状态
  • .tag[data-hovered="true"] - 悬停状态
  • .tag[data-pressed="true"] - 按下状态
  • .tag[data-focus-visible="true"] - 聚焦状态(键盘焦点)

交互状态

该组件同时支持 CSS 伪类与 data 属性:

  • 悬停:标签上 :hover 或 [data-hovered="true"]
  • 聚焦:标签上 :focus-visible 或 [data-focus-visible="true"]
  • 按下:标签上 :active 或 [data-pressed="true"]
  • 已选中:标签上 [data-selected="true"] 或 [aria-selected="true"]
  • 禁用:标签上 :disabled 或 [data-disabled="true"]

API 参考

TagGroup

Prop类型默认值描述
selectionMode"none" | "single" | "multiple""none"允许的选择类型。
selectedKeysSelection-当前选中的 key(受控)。
defaultSelectedKeysSelection-初始选中的 key(非受控)。
onSelectionChange(keys: Selection) => void-选中变化时调用的事件处理函数。
disabledKeysIterable<Key>-禁用标签的 key。
isDisabledboolean-是否禁用整个 TagGroup。
onRemove(keys: Set<Key>) => void-移除标签时调用的事件处理函数。
size"sm" | "md" | "lg""md"组内标签尺寸。
variant"default" | "surface""default"标签视觉变体。
classNamestring-额外的 Tailwind CSS 类。
childrenReactNode | RenderFunction-TagGroup 内容或渲染函数。
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>-使用自定义渲染函数覆盖默认 DOM 元素。

TagGroup.List

Prop类型默认值描述
itemsIterable<T>-标签列表要展示的数据项。
renderEmptyState() => ReactNode-列表为空时的渲染函数。
classNamestring-额外的 Tailwind CSS 类。
childrenReactNode | RenderFunction-标签列表内容或渲染函数。
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, TagListRenderProps>-使用自定义渲染函数覆盖默认 DOM 元素。

Tag

Prop类型默认值描述
idKey-标签唯一标识。
textValuestring-标签内容的字符串表示,用于无障碍。
isDisabledboolean-是否禁用该标签。
classNamestring-额外的 Tailwind CSS 类。
childrenReactNode | RenderFunction-标签内容或渲染函数。
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, TagRenderProps>-使用自定义渲染函数覆盖默认 DOM 元素。

提示: size、variant 由父级 TagGroup 继承,无法在单个 Tag 上直接设置。

Tag.RemoveButton

Prop类型默认值描述
classNamestring-额外的 Tailwind CSS 类。
childrenReactNode-自定义移除按钮内容(默认为关闭图标)。

提示: Tag.RemoveButton 支持类似 SearchField.ClearButton 的定制方式。当为 TagGroup 提供 onRemove 时:

  • 自动渲染:若 Tag 的子节点中未包含自定义 Tag.RemoveButton,会自动渲染默认移除按钮。
  • 自定义按钮:若在 Tag 下提供了自定义 Tag.RemoveButton,将替换自动渲染的按钮。
  • 自定义图标:可向 Tag.RemoveButton 传入自定义子内容(如图标)以改变外观。

示例 — 自动渲染(默认):

<TagGroup onRemove={handleRemove}>  <TagGroup.List>    <Tag id="news">News</Tag>    {/* Remove button is automatically rendered */}  </TagGroup.List></TagGroup>

示例 — 自定义 RemoveButton(带图标):

<TagGroup onRemove={handleRemove}>  <TagGroup.List>    <Tag id="news">      News      <Tag.RemoveButton>        <CustomIcon />      </Tag.RemoveButton>    </Tag>  </TagGroup.List></TagGroup>

示例 — 在 render props 中使用自定义 RemoveButton:

<Tag id="news">  {(renderProps) => (    <>      News      {!!renderProps.allowsRemoving && (        <Tag.RemoveButton>          <CustomIcon />        </Tag.RemoveButton>      )}    </>  )}</Tag>

RenderProps

在 TagGroup.List 中使用渲染函数时,会传入以下值:

Prop类型描述
isSelectedboolean标签是否选中。
isDisabledboolean标签是否禁用。
isHoveredboolean标签是否悬停。
isPressedboolean标签是否按下。
isFocusedboolean标签是否聚焦。
isFocusVisibleboolean标签是否为可见键盘焦点。

相关组件