v3.0.0-beta.4
全新的主题构建器、三个新组件(Autocomplete、Breadcrumbs、Toast)、Tabs 的 secondary 变体、Input / InputGroup 变体,以及多项改进。
已修复关键构建问题:此版本(beta.4)存在一个关键构建问题,已在 beta.5 中修复。请升级到 @lenso/[email protected] 与 @lenso/[email protected],以确保 TypeScript 声明文件能正确生成、导出能正确解析。
此版本引入了用于可视化主题定制的全新 主题构建器,三个新组件(Autocomplete、Breadcrumbs、Toast),为 Tabs 添加 secondary 变体,为 Input 与 InputGroup 添加 primary / secondary 变体,InputGroup 新增对 TextArea 的支持,以及 ⚠️ 破坏性变更:移除 Link 的下划线变体,并从表单组件中移除 isInSurface prop。
安装
升级到最新版本:
npm i @lenso/tokens@beta @lenso/ui@beta正在使用 AI 助手? 只需对它说一句「Hey Cursor,把 HeroUI 升级到最新版本」,AI 助手就会自动对比版本并应用必要的变更。了解更多请参阅 HeroUI MCP 服务器。
新增功能
主题构建器
我们很高兴推出 主题构建器 —— 用于创建与定制 HeroUI 主题的强大可视化工具。可在实时预览中构建你专属的主题,并导出可直接使用的 CSS。
主要特性:
- 可视化颜色编辑:通过 OKLCH 颜色选择器以及直观的亮度、色度、色相滑块来调整颜色
- 实时预览:在实时组件预览中立即查看你的修改
- 自定义强调色:定义你的品牌色,并观察它如何贯穿到所有组件
- 预设主题:从精选预设(如 Default、Airbnb、Coinbase、Discord)入手
- 导出即可用:生成 CSS 变量,直接复制到你的项目即可
- 浅色与深色模式:可联动也可独立地同时定制两套主题
- 键盘快捷键:支持撤销 / 重做以及快速操作,提升工作流效率
立即在 v3.heroui.com/themes 上试用。
新组件
本次发布共引入 3 个 新的基础组件:
- Autocomplete:将 Select 与过滤功能结合,让用户可以在选项列表中搜索并选择。(文档)
- Breadcrumbs:导航面包屑,用于展示当前页面在层级结构中的位置。(文档)
- Toast:用于展示临时通知与消息,支持自动关闭以及自定义放置位置。(文档)
Autocomplete
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** HeroUI v3.2.6 adaptation. SPDX-License-Identifier: Apache-2.0 */import { NativeAutocomplete } from "./_native";
export const states = [ { id: "florida", name: "Florida" }, { id: "delaware", name: "Delaware" }, { id: "california", name: "California" }, { id: "texas", name: "Texas" }, { id: "new-york", name: "New York" }, { id: "washington", name: "Washington" },];export default function Default() { return ( <NativeAutocomplete items={states} label="States to Visit" placeholder="Select states" multiple chips hideClear /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Breadcrumbs
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
import { Breadcrumbs } from "@lenso/ui";
export function BreadcrumbsBasic() { return ( <Breadcrumbs> <Breadcrumbs.Item href="#">Home</Breadcrumbs.Item> <Breadcrumbs.Item href="#">Products</Breadcrumbs.Item> <Breadcrumbs.Item href="#">Electronics</Breadcrumbs.Item> <Breadcrumbs.Item>Laptop</Breadcrumbs.Item> </Breadcrumbs> );}export default BreadcrumbsBasic;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Toast
该组件目前处于预览阶段,部分功能可能尚未按预期工作。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 variants, Apache-2.0.import { HardDrive, Persons } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Toast } from "@lenso/ui";import { Notifications, styles, type ToastData } from "./_shared";
function Messages() { const manager = Toast.useToastManager<ToastData>(); return ( <> <div {...stylex.props(styles.frame)}> <div {...stylex.props(styles.buttons)}> <Button size="sm" variant="tertiary" onClick={() => { const id = manager.add({ title: "You have been invited to join a team", description: "Bob sent you an invitation to join HeroUI team", type: "default", data: { indicator: <Persons />, actionStyle: "tertiary" }, actionProps: { children: "Dismiss", onClick: () => manager.close(id) }, }); }} > Default toast </Button> <Button size="sm" variant="secondary" onClick={() => { const id = manager.add({ title: "You have 2 credits left", description: "Get a paid plan for more credits", type: "accent", actionProps: { children: "Upgrade", onClick: () => manager.close(id) }, }); }} > Accent toast </Button> <Button xstyle={styles.successText} size="sm" variant="tertiary" onClick={() => { const id = manager.add({ title: "You have upgraded your plan", description: "You can continue using HeroUI Chat", type: "success", data: { actionStyle: "success" }, actionProps: { children: "Billing", onClick: () => manager.close(id) }, }); }} > Success toast </Button> <Button xstyle={styles.warningText} size="sm" variant="tertiary" onClick={() => { const id = manager.add({ title: "You have no credits left", description: "Upgrade to a paid plan to continue", type: "warning", data: { actionStyle: "warning" }, actionProps: { children: "Upgrade", onClick: () => manager.close(id) }, }); }} > Warning toast </Button> <Button size="sm" variant="danger-soft" onClick={() => { const id = manager.add({ title: "Storage is full", description: "Remove files to release space. Adding more text to demonstrate longer content display", type: "danger", data: { indicator: <HardDrive />, actionStyle: "danger" }, actionProps: { children: "Remove", onClick: () => manager.close(id) }, }); }} > Danger toast </Button> </div> </div> <Notifications /> </> );}
export function Variants() { return ( <Toast.Provider> <Messages /> </Toast.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
组件改进
Tabs 的 secondary 变体
为 Tabs 新增 secondary 变体,使用下划线指示器样式。该变体同时支持水平与垂直方向。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";export { Secondary } from "./source";Local adaptation source above. Derived from HeroUI v3.2.6 source.
用法:
<Tabs variant="secondary"> <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.Panel id="overview">Content</Tabs.Panel> <Tabs.Panel id="analytics">Content</Tabs.Panel></Tabs>Input 变体
为 Input 组件新增 primary 与 secondary 变体:
primary(默认):带阴影的标准样式,适用于大多数场景secondary:不带阴影的低调变体,适合在 Surface 组件内部使用
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Input } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { display: "flex", width: 240, flexDirection: "column", gap: 8 },});export function Variants() { return ( <div {...stylex.props(styles.root)}> <Input fullWidth placeholder="Primary input" variant="primary" /> <Input fullWidth placeholder="Secondary input" variant="secondary" /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
InputGroup 增强
InputGroup 组件获得多项改进:
TextArea 支持:可使用 InputGroup.TextArea 来构建带有 prefix 与 suffix 的多行文本输入。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { ArrowUp, At, Microphone, PlugConnection, Plus } from "@gravity-ui/icons";import { Button, InputGroup, Kbd, Spinner, TextField, Tooltip } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useState } from "react";const styles = stylex.create({ field: { display: "flex", flexDirection: "column", width: { default: 384, "@media (min-width: 640px)": 512 }, }, group: { display: "flex", flexDirection: "column", gap: 8, borderRadius: 24, paddingBlock: 8 }, prefix: { paddingInline: 12, paddingBlock: 0 }, input: { width: "100%", resize: "none", paddingInline: 14, paddingBlock: 0 }, suffix: { display: "flex", width: "100%", alignItems: "center", gap: 6, paddingInline: 12, paddingBlock: 0, }, actions: { marginInlineStart: "auto", display: "flex", alignItems: "center", gap: 6 }, tooltip: { display: "flex", alignItems: "center", gap: 4 }, hint: { fontSize: 12, lineHeight: "16px" }, key: { height: 16, borderRadius: "var(--radius-sm)", paddingInline: 4 },});export function WithTextArea() { const [value, setValue] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = () => { if (!value.trim()) return; setIsSubmitting(true); setTimeout(() => { setIsSubmitting(false); setValue(""); }, 1000); }; return ( <TextField fullWidth xstyle={styles.field} name="prompt"> <InputGroup fullWidth xstyle={styles.group}> <InputGroup.Prefix xstyle={styles.prefix}> <Button aria-label="Add context" size="sm" variant="outline"> <At aria-hidden="true" /> Add Context </Button> </InputGroup.Prefix> <InputGroup.TextArea aria-label="Prompt input" xstyle={styles.input} placeholder="Assign tasks or ask anything..." rows={5} value={value} onChange={(event) => setValue(event.target.value)} /> <InputGroup.Suffix xstyle={styles.suffix}> <Tooltip> <Tooltip.Trigger delay={0} render={<Button isIconOnly aria-label="Attach file" size="sm" variant="tertiary" />} > <Plus aria-hidden="true" /> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Positioner> <Tooltip.Popup> <p {...stylex.props(styles.hint)}>Add a files and more</p> </Tooltip.Popup> </Tooltip.Positioner> </Tooltip.Portal> </Tooltip> <Tooltip> <Tooltip.Trigger delay={0} render={<Button isIconOnly aria-label="Connect Apps" size="sm" variant="tertiary" />} > <PlugConnection aria-hidden="true" /> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Positioner> <Tooltip.Popup> <p {...stylex.props(styles.hint)}>Connect apps</p> </Tooltip.Popup> </Tooltip.Positioner> </Tooltip.Portal> </Tooltip> <div {...stylex.props(styles.actions)}> <Tooltip> <Tooltip.Trigger delay={0} render={<Button isIconOnly aria-label="Voice input" size="sm" variant="ghost" />} > <Microphone aria-hidden="true" /> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Positioner> <Tooltip.Popup> <p {...stylex.props(styles.hint)}>Voice input</p> </Tooltip.Popup> </Tooltip.Positioner> </Tooltip.Portal> </Tooltip> <Tooltip> <Tooltip.Trigger delay={0} render={ <Button isIconOnly aria-label="Send prompt" disabled={!value.trim()} isLoading={isSubmitting} onClick={handleSubmit} /> } > {isSubmitting ? ( <Spinner color="current" size="sm" /> ) : ( <ArrowUp aria-hidden="true" /> )} </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Positioner> <Tooltip.Popup xstyle={styles.tooltip}> <p {...stylex.props(styles.hint)}>Send</p> <Kbd xstyle={styles.key}> <Kbd.Abbr keyValue="enter" /> </Kbd> </Tooltip.Popup> </Tooltip.Positioner> </Tooltip.Portal> </Tooltip> </div> </InputGroup.Suffix> </InputGroup> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
变体:新增与 Input 组件相匹配的 primary 与 secondary 变体。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Envelope } from "@gravity-ui/icons";import { InputGroup, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ stack: { display: "flex", flexDirection: "column", gap: 16 }, field: { width: 280 }, icon: { width: 16, height: 16, color: "var(--muted)" },});export function Variants() { return ( <div {...stylex.props(styles.stack)}> <TextField xstyle={styles.field} name="primary"> <Label>Primary variant</Label> <InputGroup variant="primary"> <InputGroup.Prefix> <Envelope aria-hidden="true" {...stylex.props(styles.icon)} /> </InputGroup.Prefix> <InputGroup.Input placeholder="[email protected]" /> </InputGroup> </TextField> <TextField xstyle={styles.field} name="secondary"> <Label>Secondary variant</Label> <InputGroup variant="secondary"> <InputGroup.Prefix> <Envelope aria-hidden="true" {...stylex.props(styles.icon)} /> </InputGroup.Prefix> <InputGroup.Input placeholder="[email protected]" /> </InputGroup> </TextField> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Button 与 ButtonGroup 的 outline 变体
为 Button 与 ButtonGroup 同时新增 outline 变体,用于呈现描边样式。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Button, ButtonGroup } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";export function OutlineVariant() { return ( <div {...stylex.props(styles.column)}> <div {...stylex.props(styles.section)}> <p {...stylex.props(styles.muted)}>Button</p> <div {...stylex.props(styles.row)}> <Button variant="outline">Outline</Button> </div> </div> <div {...stylex.props(styles.section)}> <p {...stylex.props(styles.muted)}>ButtonGroup</p> <ButtonGroup variant="outline"> <Button>First</Button> <Button>Second</Button> <Button>Third</Button> </ButtonGroup> </div> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
AlertDialog 尺寸支持
为 AlertDialog 组件新增尺寸支持,让你可以控制对话框的大小。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 alert-dialog-sizes (Apache-2.0).import { Rocket } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { AlertDialog, Button } from "@lenso/ui";
const styles = stylex.create({ row: { display: "flex", flexWrap: "wrap", gap: 16 }, icon: { backgroundColor: "var(--default)", color: "var(--foreground)" }, rocket: { width: 20, height: 20 },});
export function Sizes() { const sizes = ["xs", "sm", "md", "lg", "cover"] as const; return ( <div {...stylex.props(styles.row)}> {sizes.map((size) => ( <AlertDialog key={size}> <AlertDialog.Trigger render={<Button variant="secondary" />}> {size.charAt(0).toUpperCase() + size.slice(1)} </AlertDialog.Trigger> <AlertDialog.Portal> <AlertDialog.Backdrop /> <AlertDialog.Viewport> <AlertDialog.Popup size={size}> <AlertDialog.Close aria-label="Close dialog" /> <AlertDialog.Header> <AlertDialog.Icon variant="default" xstyle={styles.icon}> <Rocket {...stylex.props(styles.rocket)} /> </AlertDialog.Icon> <AlertDialog.Title> Size: {size.charAt(0).toUpperCase() + size.slice(1)} </AlertDialog.Title> </AlertDialog.Header> <AlertDialog.Body> <AlertDialog.Description> {size === "cover" ? ( <> This alert dialog uses the <code>cover</code> size variant. It spans the full screen with margins: 16px on mobile and 40px on desktop. Maintains rounded corners and standard padding. Perfect for critical confirmations that need maximum width while preserving alert dialog aesthetics. </> ) : ( <> This alert dialog uses the <code>{size}</code> size variant. On mobile devices, all sizes adapt to near full-width for optimal viewing. On desktop, each size provides a different maximum width to suit various content needs. </> )} </AlertDialog.Description> </AlertDialog.Body> <AlertDialog.Footer> <AlertDialog.Close render={<Button variant="tertiary" />}> Cancel </AlertDialog.Close> <AlertDialog.Close render={<Button />}>Confirm</AlertDialog.Close> </AlertDialog.Footer> </AlertDialog.Popup> </AlertDialog.Viewport> </AlertDialog.Portal> </AlertDialog> ))} </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Checkbox 动画改进
为 Checkbox 提供更快的动画与更粗的描边宽度,反馈更明显。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Checkbox } from "@lenso/ui";
export function Basic() { return ( <Checkbox name="basic-terms"> <Checkbox.Content> <Checkbox.Control> <Checkbox.Indicator /> </Checkbox.Control> Accept terms and conditions </Checkbox.Content> </Checkbox> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Link 的文本装饰
Link 组件现在使用 Tailwind CSS 类来设置文本装饰,而不再依赖内置变体。这样既更灵活,也更贴合 Tailwind 的使用习惯。
可用的 Tailwind 工具类:
underline—— 始终显示下划线no-underline—— 移除下划线hover:underline—— 仅在悬停时显示下划线decoration-primary、decoration-secondary等 —— 设置下划线颜色decoration-1、decoration-2、decoration-4—— 控制下划线粗细underline-offset-1、underline-offset-2等 —— 调整下划线偏移
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Link } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";export function LinkUnderlineAndOffset() { return ( <div {...stylex.props(styles.sections)}> <div {...stylex.props(styles.section)}> <p {...stylex.props(styles.caption)}>Default hover underline</p> <Link href="#"> Hover to see the underline <Link.Icon /> </Link> </div> <div {...stylex.props(styles.section)}> <p {...stylex.props(styles.caption)}>Always visible underline</p> <Link href="#" xstyle={styles.underline}> Underline always visible <Link.Icon /> </Link> </div> <div {...stylex.props(styles.section)}> <p {...stylex.props(styles.caption)}>No underline</p> <Link href="#" xstyle={styles.noUnderline}> Link without any underline <Link.Icon /> </Link> </div> <div {...stylex.props(styles.section)}> <p {...stylex.props(styles.caption)}>Changing the underline offset</p> <div {...stylex.props(styles.column)}> {[styles.offset1, styles.offset2, styles.offset3, styles.offset4].map((offset, index) => ( <Link key={index} href="#" xstyle={offset}> Offset {index + 1} ({index + 1}px space) <Link.Icon /> </Link> ))} </div> </div> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
⚠️ 破坏性变更
Link 组件 —— 移除下划线相关变体
Link 组件内置的 underline 与 underlineOffset prop 已被移除。请改用 Tailwind CSS 类来控制文本装饰。
之前:
<Link href="#" underline="hover" underlineOffset={4}> Link text</Link>之后:
<Link href="#" className="hover:underline underline-offset-4"> Link text</Link>可用的 Tailwind 类:
underline、no-underline、hover:underline—— 装饰线decoration-primary、decoration-muted等 —— 装饰线颜色decoration-solid、decoration-dashed、decoration-dotted—— 装饰线样式decoration-1、decoration-2、decoration-4—— 装饰线粗细underline-offset-1、underline-offset-2、underline-offset-4—— 下划线偏移
详见 Link 文档。
表单组件 —— 移除 isInSurface prop
isInSurface prop 以及自动 surface 检测已从基于表单的组件中移除。当你将表单组件放置在 Surface、Card 或其他基于 Surface 的容器中时,请改用 variant="secondary"。
之前:
<Surface> {/* Input automatically detected surface context */} <Input isInSurface /></Surface>之后:
<Surface> {/* Use variant="secondary" for surface backgrounds */} <Input variant="secondary" /></Surface>受影响的组件:
- Input
- InputGroup
- TextField
- TextArea
- SearchField
- NumberField
- DateField
- TimeField
- Select
- ComboBox
- Autocomplete
secondary 变体提供不带阴影的低调样式,更适合在 surface 背景上使用。
样式修复
- Button:更新 secondary 按钮颜色,提升视觉一致性
- Checkbox:优化动画速度并加粗描边,反馈更明显(详见 Checkbox 动画改进)
- Link:更新装饰线样式与过渡时长
- Focus Visible:在 focus-visible 选择器中加入
:not(:focus),避免与 hover 状态冲突 - Separator:将固定样式仅应用到水平方向的分隔线
Bug 修复
- 修复使用按钮变体样式的 Link
- 修复 Safari 中 BEM 样式下 Fieldset 的 Flexbox 兼容性问题
- 修复 SearchField 在空状态时未正确禁用清除按钮的问题
- 修复 ButtonGroup 的 context 仅对直接子元素生效的问题
- 修复 ButtonGroup 中
BUTTON_GROUP_CHILD重新导出的类型声明
依赖更新
直接从 React Aria Components 重新导出
HeroUI 现在直接从 react-aria-components 重新导出了一系列基元与工具,方便你访问。这些导出对于 React Aria 框架配置 尤其有用。
Provider:
RouterProvider—— 配置 React Aria 的 Link 使用客户端路由器I18nProvider—— 设置 React Aria Components 使用的 locale
Hook 与工具:
isRTL—— 检查某个 locale 是否为从右到左useLocale—— 访问当前 locale 与方向useFilter—— 对集合进行过滤与排序
组件:
Collection—— 用于管理列表的集合组件ListBoxLoadMoreItem—— 用于加载更多条目的 ListBox 项
国际化工具:
getLocalizationScript—— 获取用于服务端渲染的本地化脚本(来自react-aria-components/i18n)
以上这些都可以直接从 @lenso/ui 引入:
import { RouterProvider, I18nProvider, isRTL, useLocale, useFilter, getLocalizationScript} from "@lenso/ui";链接
贡献者
感谢每一位为本次发布做出贡献的开发者!
HeroUI contributors