v3.0.0-beta.3
七个新组件、fullWidth 与 hideSeparator 支持、样式修复,以及 AlertDialog / Modal backdrop 变体调整与移除 asChild prop 等破坏性变更。
此版本引入了七个新组件(ButtonGroup、DateField、ErrorMessage、ScrollShadow、SearchField、TagGroup、TimeField),为表单组件添加 fullWidth 支持,为 Tabs、ButtonGroup 与 Accordion 引入 hideSeparator,包含若干样式修复,以及 ⚠️ 破坏性变更:移除 asChild prop,并更新了 AlertDialog 与 Modal 的 backdrop 变体。
安装
升级到最新版本:
npm i @lenso/tokens@beta @lenso/ui@beta正在使用 AI 助手? 只需对它说一句「Hey Cursor,把 HeroUI 升级到最新版本」,AI 助手就会自动对比版本并应用必要的变更。了解更多请参阅 HeroUI MCP 服务器。
新增功能
新组件
本次发布引入了 7 个 新的基础组件:
- ButtonGroup:以一致的样式与间距将相关按钮分组。(文档)
- DateField:日期输入字段,支持 label、description 与表单校验,基于 React Aria DateField 构建。(文档)
- ErrorMessage:底层的错误信息组件,用于在非表单组件中展示错误。(文档)
- ScrollShadow:通过视觉阴影提示可滚动内容溢出,并可自动检测滚动位置。(文档)
- SearchField:带有内置搜索图标与清除按钮的搜索输入字段。(文档)
- TagGroup:一组可聚焦的标签,支持键盘导航、选择与删除。(文档)
- TimeField:时间输入字段,支持 label、description 与表单校验,基于 React Aria TimeField 构建。(文档)
ButtonGroup
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { ChevronDown, ChevronLeft, ChevronRight, CodeFork, Ellipsis, Picture, Pin, QrCode, Star, TextAlignCenter, TextAlignJustify, TextAlignLeft, TextAlignRight, ThumbsDown, ThumbsUp, Video,} from "@gravity-ui/icons";import { Button, ButtonGroup, Chip, Dropdown } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { basicStyles as styles } from "./basic.stylex";import { styles as sourceStyles } from "./source.stylex";export function Basic() { return ( <div {...stylex.props(styles.root)}> <div {...stylex.props(styles.section)}> <ButtonGroup> <Button>Merge pull request</Button> <Dropdown> <Dropdown.Trigger render={<Button isIconOnly aria-label="More options" xstyle={styles.splitTrigger} />} > <ButtonGroup.Separator /> <Button.Icon> <ChevronDown /> </Button.Icon> </Dropdown.Trigger> <Dropdown.Portal> <Dropdown.Positioner side="bottom" align="end"> <Dropdown.Popup xstyle={styles.popup}> {[ { id: "merge", label: "Create a merge commit", description: "All commits from this branch will be added to the base branch", }, { id: "squash", label: "Squash and merge", description: "The 14 commits from this branch will be combined into one commit in the base branch", }, { id: "rebase", label: "Rebase and merge", description: "The 14 commits from this branch will be rebased and added to the base branch", }, ].map((item) => ( <Dropdown.Item key={item.id} xstyle={styles.item}> <span {...stylex.props(sourceStyles.menuLabel)}>{item.label}</span> <span {...stylex.props(sourceStyles.menuDescription)}> {item.description} </span> </Dropdown.Item> ))} </Dropdown.Popup> </Dropdown.Positioner> </Dropdown.Portal> </Dropdown> </ButtonGroup> </div> <div {...stylex.props(styles.section)}> <div {...stylex.props(styles.row)}> <ButtonGroup variant="tertiary"> <Button> <CodeFork {...stylex.props(styles.smallIcon)} /> Fork <Chip color="accent" size="sm" variant="soft"> 24 </Chip> </Button> <Button isIconOnly aria-label="More fork options"> <ButtonGroup.Separator /> <Button.Icon> <ChevronDown /> </Button.Icon> </Button> </ButtonGroup> <ButtonGroup variant="tertiary"> <Button isIconOnly aria-label="Show QR code"> <Button.Icon> <QrCode /> </Button.Icon> </Button> <Button> <ButtonGroup.Separator /> Scan to pay </Button> </ButtonGroup> <ButtonGroup variant="tertiary"> <Button> <Button.Icon> <ThumbsUp /> </Button.Icon> <span {...stylex.props(styles.count)}>2.4K</span> </Button> <Button isIconOnly aria-label="Dislike"> <ButtonGroup.Separator /> <Button.Icon> <ThumbsDown /> </Button.Icon> </Button> </ButtonGroup> <ButtonGroup variant="tertiary"> <Button> <Star {...stylex.props(styles.smallIcon)} /> Star </Button> <Button xstyle={styles.countButton}> <ButtonGroup.Separator /> <Chip color="accent" size="sm" variant="soft"> 104 </Chip> </Button> </ButtonGroup> <ButtonGroup variant="tertiary"> <Button> <Button.Icon> <Pin /> </Button.Icon> Pinned </Button> <Button isIconOnly aria-label="More pin options"> <ButtonGroup.Separator /> <Button.Icon> <ChevronDown /> </Button.Icon> </Button> </ButtonGroup> </div> </div> <div {...stylex.props(styles.section)}> <ButtonGroup variant="tertiary"> <Button> <Button.Icon> <ChevronLeft /> </Button.Icon> Previous </Button> <Button> <ButtonGroup.Separator /> Next <Button.Icon> <ChevronRight /> </Button.Icon> </Button> </ButtonGroup> </div> <div {...stylex.props(styles.section)}> <ButtonGroup variant="tertiary"> <Button> <Button.Icon> <Picture /> </Button.Icon> Photos </Button> <Button> <ButtonGroup.Separator /> <Button.Icon> <Video /> </Button.Icon> Videos </Button> <Button isIconOnly aria-label="More options"> <ButtonGroup.Separator /> <Button.Icon> <Ellipsis /> </Button.Icon> </Button> </ButtonGroup> </div> <div {...stylex.props(styles.section)}> <ButtonGroup variant="tertiary"> <Button>Left</Button> <Button> <ButtonGroup.Separator /> Center </Button> <Button> <ButtonGroup.Separator /> Right </Button> </ButtonGroup> </div> <div {...stylex.props(styles.section)}> <ButtonGroup variant="tertiary"> {[ { label: "Align left", icon: <TextAlignLeft /> }, { label: "Align center", icon: <TextAlignCenter /> }, { label: "Align right", icon: <TextAlignRight /> }, { label: "Justify", icon: <TextAlignJustify /> }, ].map((item, index) => ( <Button key={item.label} isIconOnly aria-label={item.label}> {index > 0 && <ButtonGroup.Separator />} <Button.Icon>{item.icon}</Button.Icon> </Button> ))} </ButtonGroup> </div> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
DateField
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// HeroUI v3.2.6. Copyright NextUI Inc. SPDX-License-Identifier: Apache-2.0.import { DateField } from "@lenso/ui";import { styles } from "./demo-styles";
export function Basic() { return ( <DateField xstyle={styles.field} name="date"> <DateField.Label>Date</DateField.Label> <DateField.Group> <DateField.Input>{(segment) => <DateField.Segment segment={segment} />}</DateField.Input> </DateField.Group> </DateField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
ErrorMessage
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6, e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e, Apache-2.0.import { Description, ErrorMessage, Label, Tag, TagGroup } from "@lenso/ui";import type { TagGroupRootProps } from "@lenso/ui";import { useId, useState } from "react";
const categories = ["News", "Travel", "Gaming", "Shopping"];export function ErrorMessageBasic() { const [selected, setSelected] = useState<TagGroupRootProps["selectedKeys"]>(new Set()); const labelId = useId(); const descriptionId = useId(); const errorId = useId(); const invalid = !selected || selected.size === 0; return ( <TagGroup selectedKeys={selected} selectionMode="multiple" onSelectionChange={setSelected} aria-labelledby={labelId} aria-describedby={`${descriptionId}${invalid ? ` ${errorId}` : ""}`} aria-invalid={invalid} > <Label id={labelId} nativeLabel={false}> Required Categories </Label> <TagGroup.List> {categories.map((category) => ( <Tag key={category} itemKey={category.toLowerCase()} textValue={category}> {category} </Tag> ))} </TagGroup.List> <Description id={descriptionId}>Select at least one category</Description> <ErrorMessage id={errorId}> {invalid && <>Please select at least one category</>} </ErrorMessage> </TagGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
SearchField
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Label, SearchField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ input: { width: 280 } });
export function Basic() { return ( <SearchField name="search"> <Label>Search</Label> <SearchField.Group> <SearchField.SearchIcon /> <SearchField.Input xstyle={styles.input} placeholder="Search..." /> <SearchField.ClearButton /> </SearchField.Group> </SearchField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
ScrollShadow
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Card, ScrollShadow } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const images = [ "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/robot1.jpeg", "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/avocado.jpeg", "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/oranges.jpeg",];const styles = stylex.create({ root: { width: "100%", maxWidth: { default: null, "@media (min-width: 640px)": 384 } }, vertical: { marginBottom: 32, width: "100%" }, heading: { marginBottom: 8, fontSize: 14, lineHeight: "20px", fontWeight: 600 }, card: { width: "100%", padding: 0 }, verticalScroll: { maxHeight: 240, padding: 16 }, horizontalScroll: { padding: 16 }, paragraphs: { display: "flex", flexDirection: "column", gap: 16 }, cards: { display: "flex", flexDirection: "row", gap: 16 }, item: { display: "flex", minWidth: 200, flexDirection: "row", gap: 12, padding: 4 }, image: { aspectRatio: "1", width: { default: 64, "@media (min-width: 640px)": 80 }, height: { default: 64, "@media (min-width: 640px)": 80 }, flexShrink: 0, borderRadius: "var(--radius-xl)", objectFit: "cover", userSelect: "none", }, text: { display: "flex", flex: 1, flexDirection: "column", justifyContent: "center", gap: 4 }, title: { fontSize: 14 }, description: { fontSize: 12 },});export default function Orientation() { return ( <div {...stylex.props(styles.root)}> <div {...stylex.props(styles.vertical)}> <h4 {...stylex.props(styles.heading)}>Vertical</h4> <Card xstyle={styles.card}> <ScrollShadow tabIndex={0} aria-label="Vertical sample" xstyle={styles.verticalScroll} orientation="vertical" > <div {...stylex.props(styles.paragraphs)}> {Array.from({ length: 10 }, (_, index) => ( <p key={index}> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Morbi accumsan cursus enim, sed ultricies sapien. </p> ))} </div> </ScrollShadow> </Card> </div> <div> <h4 {...stylex.props(styles.heading)}>Horizontal</h4> <Card xstyle={styles.card}> <ScrollShadow tabIndex={0} aria-label="Horizontal cards" xstyle={styles.horizontalScroll} orientation="horizontal" > <div {...stylex.props(styles.cards)}> {Array.from({ length: 10 }, (_, index) => ( <Card key={index} xstyle={styles.item} variant="transparent"> <img alt="Lorem Card" {...stylex.props(styles.image)} loading="lazy" src={images[index % images.length]} /> <div {...stylex.props(styles.text)}> <Card.Title xstyle={styles.title}>Bridging the Future</Card.Title> <Card.Description xstyle={styles.description}>Today, 6:30 PM</Card.Description> </div> </Card> ))} </div> </ScrollShadow> </Card> </div> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
TagGroup
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"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.
TimeField
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// HeroUI v3.2.6. Copyright NextUI Inc. SPDX-License-Identifier: Apache-2.0.import { TimeField } from "@lenso/ui";import { styles } from "../date-field/demo-styles";
export function Basic() { return ( <TimeField xstyle={styles.field} name="time"> <TimeField.Label>Time</TimeField.Label> <TimeField.Group> <TimeField.Input>{(segment) => <TimeField.Segment segment={segment} />}</TimeField.Input> </TimeField.Group> </TimeField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
全宽支持
为表单与输入组件新增 fullWidth 支持,可以让它们撑满容器的整个宽度。这在构建一致的表单布局与响应式设计时尤其有用。
支持的组件:
- ButtonGroup
- Button
- ComboBox
- DateField
- DateInputGroup
- InputGroup
- Input
- NumberField
- SearchField
- Select
- TextField
- TextArea
- TimeField
组件改进
分隔线控制增强
为 Tabs、ButtonGroup 与 Accordion 组件新增 hideSeparator 支持,可隐藏条目之间的分隔线,呈现更简洁、更纯粹的外观。
Tabs:
<Tabs hideSeparator> <Tabs.ListContainer> <Tabs.List aria-label="Options"> <Tabs.Tab id="overview">Overview<Tabs.Indicator /></Tabs.Tab> <Tabs.Tab id="analytics">Analytics<Tabs.Indicator /></Tabs.Tab> </Tabs.List> </Tabs.ListContainer></Tabs>ButtonGroup:
<ButtonGroup hideSeparator> <Button>First</Button> <Button>Second</Button> <Button>Third</Button></ButtonGroup>Accordion:
<Accordion hideSeparator> <Accordion.Item> <Accordion.Heading> <Accordion.Trigger>Item 1</Accordion.Trigger> </Accordion.Heading> <Accordion.Panel> <Accordion.Body>Content</Accordion.Body> </Accordion.Panel> </Accordion.Item></Accordion>文档图标集成
将 @gravity-ui/icons 集成到文档组件中,统一图标渲染,同时改进了 SSR 支持并提升了性能。
依赖更新
React Aria Components v1.14.0
将 React Aria Components 升级到 v1.14.0。本次升级包含:
增强:
- SearchField:新增
isReadOnly与isRequired渲染属性 - Tooltip:新增
shouldCloseOnPress属性 - Tabs:支持在 tab 面板之间进行动画过渡
- 其他:
useControlledState现已在setState回调中提供支持
修复:
- ComboBox:修复 VoiceOver 不读取 ListBox 项
aria-label的问题 - 日期与时间:增强了对 absolute 日期与日期时间字符串的错误处理
- NumberField:在移动端滚动时不再误触发递增 / 递减
- Overlay:修复了设置 boundary container 时 overlay 定位与 flip 的问题
- Table:修复了在键盘导航期间进行拖放时的崩溃问题
- 其他多项 bug 修复与改进
完整变更请参阅 React Aria Components v1.14.0 发布说明。
其他依赖升级
@internationalized/date:3.10.0 → 3.10.1@radix-ui/react-avatar:1.1.10 → 1.1.11tailwind-merge:3.3.1 → 3.4.0tailwind-variants:3.1.1 → 3.2.2
样式修复
表单组件的禁用状态
修复了 Input 与 TextArea 组件的禁用状态样式。
样式优化
- 提高选择器精确度:增强 CSS 选择器特异性,让样式隔离更好、性能更优
- 动画增强:改进了多个组件的动画性能与流畅度
- 新增 no-highlight 工具类:新增
no-highlight工具类,用于防止交互元素中的文字被选中,从而提升体验 - 优化 will-change 属性:在多个组件中调整
will-changeCSS 属性,以获得更好的动画性能 - 移除全局滚动条样式:移除了全局滚动条样式,避免与自定义滚动条实现冲突,并修复了 modal / overlay 的交互问题
⚠️ 破坏性变更
AlertDialog 与 Modal 的 backdrop 变体
backdropVariant / variant prop 的取值已从 "solid" 重命名为 "opaque",以提升语义清晰度——「opaque」(不透明)更准确地描述了遮罩的视觉外观。
迁移:
将 AlertDialog 中所有 backdropVariant="solid" 替换为 backdropVariant="opaque",将 Modal 中所有 variant="solid" 替换为 variant="opaque":
// Before<AlertDialog.Backdrop backdropVariant="solid"> <AlertDialog.Container> {/* content */} </AlertDialog.Container></AlertDialog.Backdrop>
<Modal.Backdrop variant="solid"> <Modal.Container> {/* content */} </Modal.Container></Modal.Backdrop>
// After<AlertDialog.Backdrop backdropVariant="opaque"> <AlertDialog.Container> {/* content */} </AlertDialog.Container></AlertDialog.Backdrop>
<Modal.Backdrop variant="opaque"> <Modal.Container> {/* content */} </Modal.Container></Modal.Backdrop>可用的 backdrop 变体:
"opaque"—— 深色不透明遮罩,完全遮挡背景(即此前的"solid")"blur"—— 模糊遮罩,柔和地遮挡背景"transparent"—— 透明遮罩,保持背景可见
移除 asChild prop
为提供更清晰的 API、更强的类型安全性以及更简单的使用方式,组件中的 asChild 模式已被移除。
关于组件组合模式的更多细节,请参阅 组合指南。
Bug 修复
- 修复了
isInvalid样式在 surface 背景上使用相关组件时的表现 - 修复了 AlertDialog 与 Modal 关闭后重新渲染的问题
- 修复了浮层关闭时未能正确清理的问题
- 修复了文档中 Storybook 链接与导航的问题
链接
贡献者
感谢每一位为本次发布做出贡献的开发者!
HeroUI contributors