Skip to content
Lenso UI

v3.0.0-beta.3

七个新组件、fullWidth 与 hideSeparator 支持、样式修复,以及 AlertDialog / Modal backdrop 变体调整与移除 asChild prop 等破坏性变更。

2025 年 12 月 19 日

此版本引入了七个新组件(ButtonGroup、DateField、ErrorMessage、ScrollShadow、SearchField、TagGroup、TimeField),为表单组件添加 fullWidth 支持,为 Tabs、ButtonGroup 与 Accordion 引入 hideSeparator,包含若干样式修复,以及 ⚠️ 破坏性变更:移除 asChild prop,并更新了 AlertDialog 与 Modal 的 backdrop 变体。

HeroUI v3 Beta 3

安装

升级到最新版本:

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 支持,可以让它们撑满容器的整个宽度。这在构建一致的表单布局与响应式设计时尤其有用。

支持的组件:

组件改进

分隔线控制增强

为 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.11
  • tailwind-merge:3.3.1 → 3.4.0
  • tailwind-variants:3.1.1 → 3.2.2

样式修复

表单组件的禁用状态

修复了 Input 与 TextArea 组件的禁用状态样式。

样式优化

  • 提高选择器精确度:增强 CSS 选择器特异性,让样式隔离更好、性能更优
  • 动画增强:改进了多个组件的动画性能与流畅度
  • 新增 no-highlight 工具类:新增 no-highlight 工具类,用于防止交互元素中的文字被选中,从而提升体验
  • 优化 will-change 属性:在多个组件中调整 will-change CSS 属性,以获得更好的动画性能
  • 移除全局滚动条样式:移除了全局滚动条样式,避免与自定义滚动条实现冲突,并修复了 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