v3.0.0-beta.7
4 new components (Calendar, RangeCalendar, DatePicker, DateRangePicker) and APIs improvements.
This release adds 4 new components: Calendar, RangeCalendar, DatePicker, and DateRangePicker. Also new: Switch.Content for grouping label and description, and Tabs.Separator for opt-in separator lines between tabs.
⚠️ Breaking changes: hideSeparator removed from Tabs; DateInputGroup and ColorInputGroup moved under DateField.Group, TimeField.Group, and ColorField.Group.
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
Date & Time System
Date & Time — Calendar, DatePicker, RangeCalendar, and DateRangePicker built on React Aria date primitives. Supports i18n, time zones, and full keyboard/ARIA accessibility.
Key features:
- Calendar systems: Gregorian, Buddhist, Persian, and others
- Year picker: Overlay for quick year navigation
- Cell indicators: Events, availability, or status dots on cells
- Range selection: Date ranges with visual highlighting
- Accessible: Keyboard nav, screen readers, ARIA
All date values use @internationalized/date types (CalendarDate, CalendarDateTime, ZonedDateTime). Wrap with I18nProvider to override locale; read it with useLocale.
New Components
- Calendar: Single date selection, year picker, indicators, multi-month. (Docs)
- RangeCalendar: Date ranges with range highlighting and multi-month. (Docs)
- DatePicker: Date field + popover calendar. (Docs)
- DateRangePicker: Two date fields + popover range calendar. (Docs)
Calendar
Single-date calendar with year picker, cell indicators, multi-month view, and i18n calendars.
"use client";
/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { Calendar } from "@lenso/ui";
export function Basic() { return ( <Calendar aria-label="Event date"> <Calendar.Header> <Calendar.Heading /> <Calendar.NavButton slot="previous" /> <Calendar.NavButton slot="next" /> </Calendar.Header> <Calendar.Grid> <Calendar.GridHeader> {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>} </Calendar.GridHeader> <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody> </Calendar.Grid> </Calendar> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Year Picker:
"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { Calendar } from "@lenso/ui";
export function YearPicker() { return ( <Calendar aria-label="Event date"> <Calendar.Header> <Calendar.YearPickerTrigger> <Calendar.YearPickerTriggerHeading /> <Calendar.YearPickerTriggerIndicator /> </Calendar.YearPickerTrigger> <Calendar.NavButton slot="previous" /> <Calendar.NavButton slot="next" /> </Calendar.Header> <Calendar.Grid> <Calendar.GridHeader> {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>} </Calendar.GridHeader> <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody> </Calendar.Grid> <Calendar.YearPickerGrid> <Calendar.YearPickerGridBody> {({ year }) => <Calendar.YearPickerCell year={year} />} </Calendar.YearPickerGridBody> </Calendar.YearPickerGrid> </Calendar> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
International Calendars:
"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { Calendar } from "@lenso/ui";import { getLocalTimeZone, today } from "@internationalized/date";import { I18nProvider } from "react-aria-components/I18nProvider";
export function InternationalCalendar() { return ( <I18nProvider locale="hi-IN-u-ca-indian"> <Calendar aria-label="Event date" defaultValue={today(getLocalTimeZone())}> <Calendar.Header> <Calendar.YearPickerTrigger> <Calendar.YearPickerTriggerHeading /> <Calendar.YearPickerTriggerIndicator /> </Calendar.YearPickerTrigger> <Calendar.NavButton slot="previous" /> <Calendar.NavButton slot="next" /> </Calendar.Header> <Calendar.Grid> <Calendar.GridHeader> {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>} </Calendar.GridHeader> <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody> </Calendar.Grid> <Calendar.YearPickerGrid> <Calendar.YearPickerGridBody> {({ year }) => <Calendar.YearPickerCell year={year} />} </Calendar.YearPickerGridBody> </Calendar.YearPickerGrid> </Calendar> </I18nProvider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
RangeCalendar
Date range selection with range highlighting and multi-month views.
"use client";
/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. */import { RangeCalendar } from "@lenso/ui";
export function Basic() { return ( <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon"> <RangeCalendar.Header> <RangeCalendar.Heading /> <RangeCalendar.NavButton slot="previous" /> <RangeCalendar.NavButton slot="next" /> </RangeCalendar.Header> <RangeCalendar.Grid> <RangeCalendar.GridHeader> {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>} </RangeCalendar.GridHeader> <RangeCalendar.GridBody> {(date) => <RangeCalendar.Cell date={date} />} </RangeCalendar.GridBody> </RangeCalendar.Grid> </RangeCalendar> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Multiple Months:
"use client";/** HeroUI v3.2.6 derived work. Copyright NextUI Inc. Apache-2.0. Modified for StyleX. */import { RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { containerType: "normal", width: "100%", maxWidth: "none", overflowX: "auto" }, months: { marginInline: "auto", display: "flex", width: "max-content", gap: 32 }, month: { width: 256 }, heading: { flex: "none" }, spacer: { width: 24, height: 24 },});export function MultipleMonths() { return ( <RangeCalendar aria-label="Trip dates" xstyle={styles.root} visibleDuration={{ months: 2 }}> <div {...stylex.props(styles.months)}> <div {...stylex.props(styles.month)}> <RangeCalendar.Header> <RangeCalendar.NavButton slot="previous" /> <RangeCalendar.Heading xstyle={styles.heading} /> <div {...stylex.props(styles.spacer)} /> </RangeCalendar.Header> <RangeCalendar.Grid> <RangeCalendar.GridHeader> {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>} </RangeCalendar.GridHeader> <RangeCalendar.GridBody> {(date) => <RangeCalendar.Cell date={date} />} </RangeCalendar.GridBody> </RangeCalendar.Grid> </div> <div {...stylex.props(styles.month)}> <RangeCalendar.Header> <div {...stylex.props(styles.spacer)} /> <RangeCalendar.Heading xstyle={styles.heading} offset={{ months: 1 }} /> <RangeCalendar.NavButton slot="next" /> </RangeCalendar.Header> <RangeCalendar.Grid offset={{ months: 1 }}> <RangeCalendar.GridHeader> {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>} </RangeCalendar.GridHeader> <RangeCalendar.GridBody> {(date) => <RangeCalendar.Cell date={date} />} </RangeCalendar.GridBody> </RangeCalendar.Grid> </div> </div> </RangeCalendar> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
DatePicker
Date field + popover calendar. Format options, i18n, custom indicators, validation.
"use client";
/** Adapted from the pinned HeroUI v3.2.6 basic.json. Copyright NextUI Inc. Apache-2.0. Local RAC label and StyleX replace source Label and utility classes. */import { Calendar, DateField, DatePicker } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 288 } });export function Basic() { return ( <DatePicker xstyle={styles.field} name="date"> <DatePicker.Label>Date</DatePicker.Label> <DateField.Group fullWidth> <DateField.Input>{(segment) => <DateField.Segment segment={segment} />}</DateField.Input> <DateField.Suffix> <DatePicker.Trigger> <DatePicker.TriggerIndicator /> </DatePicker.Trigger> </DateField.Suffix> </DateField.Group> <DatePicker.Popover> <Calendar aria-label="Event date"> <Calendar.Header> <Calendar.YearPickerTrigger> <Calendar.YearPickerTriggerHeading /> <Calendar.YearPickerTriggerIndicator /> </Calendar.YearPickerTrigger> <Calendar.NavButton slot="previous" /> <Calendar.NavButton slot="next" /> </Calendar.Header> <Calendar.Grid> <Calendar.GridHeader> {(day) => <Calendar.HeaderCell>{day}</Calendar.HeaderCell>} </Calendar.GridHeader> <Calendar.GridBody>{(date) => <Calendar.Cell date={date} />}</Calendar.GridBody> </Calendar.Grid> <Calendar.YearPickerGrid> <Calendar.YearPickerGridBody> {({ year }) => <Calendar.YearPickerCell year={year} />} </Calendar.YearPickerGridBody> </Calendar.YearPickerGrid> </Calendar> </DatePicker.Popover> </DatePicker> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Date and Time (with TimeField):
"use client";/** Adapted from HeroUI v3.2.6 format-options.json. Copyright NextUI Inc. Apache-2.0. */import { DatePicker, TimeField } from "@lenso/ui";import { getLocalTimeZone, parseDate, parseZonedDateTime, type DateValue,} from "@internationalized/date";import { useMemo, useState } from "react";import * as stylex from "@stylexjs/stylex";import { PickerCalendar, PickerInput } from "./parts";import { FormatControls, formatStyles, type Granularity, type HourCycle } from "./format-controls";
export function FormatOptions() { const [granularity, setGranularity] = useState<Granularity>("minute"); const [hourCycle, setHourCycle] = useState<HourCycle>(12); const [hideTimeZone, setHideTimeZone] = useState(false); const [shouldForceLeadingZeros, setShouldForceLeadingZeros] = useState(false); const timeGranularity = granularity !== "day" ? granularity : undefined; const defaultValue = useMemo<DateValue>( () => granularity === "day" ? parseDate("2026-02-03") : parseZonedDateTime(`2026-02-03T08:45:00[${getLocalTimeZone()}]`), [granularity], ); return ( <div {...stylex.props(formatStyles.stack)}> <DatePicker key={granularity} xstyle={formatStyles.field} defaultValue={defaultValue} granularity={granularity} hourCycle={hourCycle} hideTimeZone={hideTimeZone} shouldForceLeadingZeros={shouldForceLeadingZeros} name="date" > {({ state }) => ( <> <DatePicker.Label>Date and time</DatePicker.Label> <PickerInput /> <DatePicker.Popover xstyle={formatStyles.popover}> <PickerCalendar /> {timeGranularity && ( <div {...stylex.props(formatStyles.timeRow)}> <span {...stylex.props(formatStyles.label)}>Time</span> <TimeField aria-label="Time" granularity={timeGranularity} hourCycle={hourCycle} hideTimeZone={hideTimeZone} name="time" shouldForceLeadingZeros={shouldForceLeadingZeros} value={state.timeValue} onChange={(value) => { if (value) state.setTimeValue(value); }} > <TimeField.Group variant="secondary"> <TimeField.Input> {(segment) => <TimeField.Segment segment={segment} />} </TimeField.Input> </TimeField.Group> </TimeField> </div> )} </DatePicker.Popover> </> )} </DatePicker> <FormatControls {...{ granularity, setGranularity, hourCycle, setHourCycle, hideTimeZone, setHideTimeZone, shouldForceLeadingZeros, setShouldForceLeadingZeros, }} /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
DateRangePicker
Two date fields + popover range calendar.
"use client";
/** Adapted from the pinned HeroUI v3.2.6 basic.json. Copyright NextUI Inc. Apache-2.0. Local RAC label and StyleX replace source Label and utility classes. */import { DateField, DateRangePicker, RangeCalendar } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 320 } });export function Basic() { return ( <DateRangePicker xstyle={styles.field} endName="endDate" startName="startDate"> <DateRangePicker.Label>Trip dates</DateRangePicker.Label> <DateField.Group fullWidth> <DateField.Input slot="start"> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> <DateRangePicker.RangeSeparator /> <DateField.Input slot="end"> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> <DateField.Suffix> <DateRangePicker.Trigger> <DateRangePicker.TriggerIndicator /> </DateRangePicker.Trigger> </DateField.Suffix> </DateField.Group> <DateRangePicker.Popover> <RangeCalendar aria-label="Trip dates"> <RangeCalendar.Header> <RangeCalendar.YearPickerTrigger> <RangeCalendar.YearPickerTriggerHeading /> <RangeCalendar.YearPickerTriggerIndicator /> </RangeCalendar.YearPickerTrigger> <RangeCalendar.NavButton slot="previous" /> <RangeCalendar.NavButton slot="next" /> </RangeCalendar.Header> <RangeCalendar.Grid> <RangeCalendar.GridHeader> {(day) => <RangeCalendar.HeaderCell>{day}</RangeCalendar.HeaderCell>} </RangeCalendar.GridHeader> <RangeCalendar.GridBody> {(date) => <RangeCalendar.Cell date={date} />} </RangeCalendar.GridBody> </RangeCalendar.Grid> <RangeCalendar.YearPickerGrid> <RangeCalendar.YearPickerGridBody> {({ year }) => <RangeCalendar.YearPickerCell year={year} />} </RangeCalendar.YearPickerGridBody> </RangeCalendar.YearPickerGrid> </RangeCalendar> </DateRangePicker.Popover> </DateRangePicker> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
API Improvements
Switch.Content
Switch.Content groups label and description next to the switch control (#6240).
Before:
import { Switch, Label, Description } from '@lenso/ui';
<Switch> <Switch.Control> <Switch.Thumb /> </Switch.Control> <Switch.Content> <Label>Email notifications</Label> <Description>Get notified when someone mentions you</Description> </Switch.Content></Switch>Tabs.Separator
The Tabs component now includes an explicit Tabs.Separator sub-component for adding visual separator lines between tabs. This replaces the previous automatic CSS pseudo-element separator and the hideSeparator prop (#6243).
Separators are now opt-in — add <Tabs.Separator /> inside each <Tabs.Tab> where you want a separator line.
"use client";export { WithSeparator } from "./source";Local adaptation source above. Derived from HeroUI v3.2.6 source.
Field Sub-Component Consolidation
DateField, TimeField, and ColorField now expose their input group sub-components directly, removing the need to import DateInputGroup or ColorInputGroup separately. See Breaking Changes for migration details.
Breadcrumbs Fix
Props passed to Breadcrumbs.Item are now forwarded to the underlying Link (#6233).
Style Fixes
- ListBox Item: Adjusted hover background color from
bg-default-hovertobg-defaultfor consistency - Date Input Group: Changed segment text from
tabular-numstotext-nowrapfor better layout - Date Input Group: Improved focus-within styles to exclude date picker trigger from field focus highlighting
Dependencies
- React Aria Components: Updated from
1.14.0to1.15.0— adds a newrenderprop for customizing the DOM element rendered by any React Aria component (useful for router links, animation libraries like Motion, etc.) - @react-aria/utils: Updated from
3.32.0to3.33.0 - @react-types/shared: Updated from
3.32.1to3.33.0 - @internationalized/date: Updated from
3.10.1to3.11.0— date fields now constrain on blur instead of as you type - Added
@react-aria/i18nand@react-stately/utilsfor calendar i18n
⚠️ Breaking Changes
Tabs — hideSeparator Prop Removed
The hideSeparator prop has been removed from the Tabs component. Separators are now opt-in using the new <Tabs.Separator /> sub-component instead of being automatically generated via CSS pseudo-elements (#6243).
Before:
{/* Separators shown by default, hidden via prop */}<Tabs hideSeparator> <Tabs.List> <Tabs.Tab id="tab1">Tab 1<Tabs.Indicator /></Tabs.Tab> <Tabs.Tab id="tab2">Tab 2<Tabs.Indicator /></Tabs.Tab> </Tabs.List></Tabs>After:
{/* No separators by default — explicitly add them where needed */}<Tabs> <Tabs.List> <Tabs.Tab id="tab1"><Tabs.Indicator />Tab 1</Tabs.Tab> <Tabs.Tab id="tab2"><Tabs.Separator /><Tabs.Indicator />Tab 2</Tabs.Tab> </Tabs.List></Tabs>CSS Changes:
- Tab separator styles moved from pseudo-element (
.tabs__tab:not(:first-child):before) to a dedicated.tabs__separatorclass - The
[data-hide-separator]data attribute has been removed
Field Sub-Component API Changes
DateInputGroup and ColorInputGroup are no longer exported directly from @lenso/ui. Their sub-components have been consolidated under their respective field components (DateField, TimeField, ColorField).
DateField Changes
Before:
import {DateField, Label, DateInputGroup, Description} from '@lenso/ui';
<DateField> <Label>Date</Label> <DateInputGroup> <DateInputGroup.Prefix>...</DateInputGroup.Prefix> <DateInputGroup.Input> {(segment) => <DateInputGroup.Segment segment={segment} />} </DateInputGroup.Input> <DateInputGroup.Suffix>...</DateInputGroup.Suffix> </DateInputGroup> <Description>Pick a date</Description></DateField>After:
import {DateField, Label, Description} from '@lenso/ui';
<DateField> <Label>Date</Label> <DateField.Group> <DateField.Prefix>...</DateField.Prefix> <DateField.Input> {(segment) => <DateField.Segment segment={segment} />} </DateField.Input> <DateField.Suffix>...</DateField.Suffix> </DateField.Group> <Description>Pick a date</Description></DateField>TimeField Changes
Same pattern as DateField:
| Before | After |
|---|---|
DateInputGroup | TimeField.Group |
DateInputGroup.Input | TimeField.Input |
DateInputGroup.Segment | TimeField.Segment |
DateInputGroup.Prefix | TimeField.Prefix |
DateInputGroup.Suffix | TimeField.Suffix |
ColorField Changes
| Before | After |
|---|---|
ColorInputGroup | ColorField.Group |
ColorInputGroup.Input | ColorField.Input |
ColorInputGroup.Prefix | ColorField.Prefix |
ColorInputGroup.Suffix | ColorField.Suffix |
Usage:
import {ColorField, Label, ColorInputGroup, ColorSwatch} from '@lenso/ui';
<ColorField> <Label>Color</Label> <ColorInputGroup> <ColorInputGroup.Prefix> <ColorSwatch color="#000" /> </ColorInputGroup.Prefix> <ColorInputGroup.Input /> </ColorInputGroup></ColorField>After:
import {ColorField, Label, ColorSwatch} from '@lenso/ui';
<ColorField> <Label>Color</Label> <ColorField.Group> <ColorField.Prefix> <ColorSwatch color="#000" /> </ColorField.Prefix> <ColorField.Input /> </ColorField.Group></ColorField>Note: The underlying CSS classes (.date-input-group, .color-input-group, etc.) remain unchanged. Only the JavaScript import paths and component names have changed.
Links
Contributors
Thanks to everyone who contributed to this release!
HeroUI contributors