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" | 允许的选择类型。 |
selectedKeys | Selection | - | 当前选中的 key(受控)。 |
defaultSelectedKeys | Selection | - | 初始选中的 key(非受控)。 |
onSelectionChange | (keys: Selection) => void | - | 选中变化时调用的事件处理函数。 |
disabledKeys | Iterable<Key> | - | 禁用标签的 key。 |
isDisabled | boolean | - | 是否禁用整个 TagGroup。 |
onRemove | (keys: Set<Key>) => void | - | 移除标签时调用的事件处理函数。 |
size | "sm" | "md" | "lg" | "md" | 组内标签尺寸。 |
variant | "default" | "surface" | "default" | 标签视觉变体。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | RenderFunction | - | TagGroup 内容或渲染函数。 |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined> | - | 使用自定义渲染函数覆盖默认 DOM 元素。 |
TagGroup.List
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
items | Iterable<T> | - | 标签列表要展示的数据项。 |
renderEmptyState | () => ReactNode | - | 列表为空时的渲染函数。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | RenderFunction | - | 标签列表内容或渲染函数。 |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, TagListRenderProps> | - | 使用自定义渲染函数覆盖默认 DOM 元素。 |
Tag
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
id | Key | - | 标签唯一标识。 |
textValue | string | - | 标签内容的字符串表示,用于无障碍。 |
isDisabled | boolean | - | 是否禁用该标签。 |
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | RenderFunction | - | 标签内容或渲染函数。 |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, TagRenderProps> | - | 使用自定义渲染函数覆盖默认 DOM 元素。 |
提示: size、variant 由父级 TagGroup 继承,无法在单个 Tag 上直接设置。
Tag.RemoveButton
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | 额外的 Tailwind CSS 类。 |
children | ReactNode | - | 自定义移除按钮内容(默认为关闭图标)。 |
提示: 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 | 类型 | 描述 |
|---|---|---|
isSelected | boolean | 标签是否选中。 |
isDisabled | boolean | 标签是否禁用。 |
isHovered | boolean | 标签是否悬停。 |
isPressed | boolean | 标签是否按下。 |
isFocused | boolean | 标签是否聚焦。 |
isFocusVisible | boolean | 标签是否为可见键盘焦点。 |