v3.0.0-beta.3
Seven new components, fullWidth and hideSeparator support, style fixes, and breaking changes for AlertDialog/Modal backdrop variants and asChild prop removal.
This release introduces seven new components (ButtonGroup, DateField, ErrorMessage, ScrollShadow, SearchField, TagGroup, TimeField), adds fullWidth support for form components and hideSeparator to Tabs, ButtonGroup, and Accordion, includes style fixes, and ⚠️ breaking changes removing the asChild prop and updating AlertDialog & Modal backdrop variants.
Installation
Update to the latest version:
npm i @lenso/tokens@beta @lenso/ui@betaUsing AI assistants? Simply prompt "Hey Cursor, update HeroUI to the latest version" and your AI assistant will automatically compare versions and apply the necessary changes. Learn more about the HeroUI MCP Server.
What's New
New Components
This release introduces 7 new essential components:
- ButtonGroup: Groups related buttons with consistent styling and spacing. (Documentation)
- DateField: Date input field with labels, descriptions, and validation built on React Aria DateField. (Documentation)
- ErrorMessage: Low-level error message component for displaying errors in non-form components. (Documentation)
- ScrollShadow: Apply visual shadows to indicate scrollable content overflow with automatic detection of scroll position. (Documentation)
- SearchField: Search input field with built-in search icon and clear button. (Documentation)
- TagGroup: Focusable list of tags with support for keyboard navigation, selection, and removal. (Documentation)
- TimeField: Time input field with labels, descriptions, and validation built on React Aria TimeField. (Documentation)
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.
Full-Width Support
Added fullWidth support to form and input components, allowing them to span the full width of their container. This is particularly useful for creating consistent form layouts and responsive designs.
Supported components:
- ButtonGroup
- Button
- ComboBox
- DateField
- DateInputGroup
- InputGroup
- Input
- NumberField
- SearchField
- Select
- TextField
- TextArea
- TimeField
Component Improvements
Separator Control Enhancement
Added hideSeparator prop to Tabs, ButtonGroup, and Accordion components, allowing you to hide separator lines between items for a cleaner, more minimal appearance.
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>Documentation Icons Integration
Integrated @gravity-ui/icons into documentation components for consistent icon rendering with improved SSR support and better performance.
Dependencies
React Aria Components v1.14.0
Upgraded react-aria-components to v1.14.0. This release includes:
Enhancements:
- SearchField: Added
isReadOnlyandisRequiredto render props - Tooltip: Added
shouldCloseOnPressprop - Tabs: Support for animated transitions between tab panels
- Miscellaneous: Support for
setStatecallback inuseControlledState
Fixes:
- ComboBox: Fixed VoiceOver announcement not respecting
aria-labelof ListBoxItem - Date and Time: Enhanced error handling for absolute date and date time strings
- NumberField: Prevented incrementing/decrementing when scrolling on mobile
- Overlays: Fixed overlay positioning and flipping when boundary container is set
- Table: Fixed crash in drag and drop when keyboard navigating
- Various other bug fixes and improvements
For the complete list of changes, see the React Aria Components v1.14.0 release notes.
Other Dependency Upgrades
@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
Style Fixes
Form Component Disabled State
Fixed disabled state styling for Input and TextArea components.
Style Optimizations
- Improved selector precision: Enhanced CSS selector specificity for better style isolation and performance
- Enhanced animations: Improved animation performance and smoothness across components
- Added no-highlight utility: Added
no-highlightutility class to prevent text selection on interactive elements for improved user experience - Optimized will-change properties: Updated
will-changeCSS properties across components for better animation performance - Removed global scrollbar styling: Removed global scrollbar styles to prevent conflicts with custom scrollbar implementations and fix modal/overlay interaction issues
⚠️ Breaking Changes
AlertDialog & Modal Backdrop Variant
Renamed backdropVariant/variant prop value from "solid" to "opaque" for better semantic clarity. The term "opaque" more accurately describes the backdrop's visual appearance.
Migration:
Update all instances of backdropVariant="solid" to backdropVariant="opaque" for AlertDialog, and variant="solid" to variant="opaque" for Modal:
// 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>Available backdrop variants:
"opaque"- Dark backdrop that completely obscures the background (formerly"solid")"blur"- Blurred backdrop that softly obscures the background"transparent"- Transparent backdrop that keeps the background visible
Removed asChild Prop
Removed the asChild prop pattern from components for cleaner APIs, improved type safety, and simplified usage.
For more details about component composition patterns, see the Composition guide.
Bug Fixes
- Fixed
isInvalidstyles when components are used on surface backgrounds - Fixed AlertDialog and Modal re-rendering issues after close
- Fixed overlay close issue that prevented proper cleanup when closing overlays
- Fixed Storybook links and navigation issues
Links
Contributors
Thanks to everyone who contributed to this release!
HeroUI contributors