Skip to content
Lenso UI

v3.0.0-beta.4

全新的主题构建器、三个新组件(Autocomplete、Breadcrumbs、Toast)、Tabs 的 secondary 变体、Input / InputGroup 变体,以及多项改进。

2026 年 1 月 20 日

已修复关键构建问题:此版本(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。

HeroUI v3 Beta 4

安装

升级到最新版本:

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.

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

"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 组件现在使用 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 组件内置的 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