v3.0.0-alpha.34
Essentials for building forms with a clean API Form, TextField, RadioGroup, Label, Input, Fieldset and more.
This release introduces Form-based components, form field tokens, reorganizes Storybook, and aligns data-slot markers across components.
Installation
Update to the latest version:
npm i @lenso/tokens@alpha @lenso/ui@alphaUsing 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
Form-based Components
We've introduced a comprehensive set of form-based components built on React Aria Components, providing accessible and composable building blocks for creating forms. These components include Description, FieldError, Fieldset, Form, Input, Label, RadioGroup, TextField, and TextArea.
Description
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Description, Input, Label } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useId } from "react";import { demoStyles } from "../../demo.stylex";
export function Basic() { const id = useId(); return ( <div {...stylex.props(demoStyles.column)}> <Label htmlFor={id}>Email</Label> <Input aria-describedby={`${id}-description`} xstyle={demoStyles.field} id={id} placeholder="[email protected]" type="email" /> <Description id={`${id}-description`}> We'll never share your email with anyone else. </Description> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
FieldError
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { FieldError, Input, Label, TextField } from "@lenso/ui";import { useState } from "react";import { demoStyles } from "../../demo.stylex";
export function Basic() { const [value, setValue] = useState("jr"); const invalid = value.length > 0 && value.length < 3; return ( <TextField xstyle={demoStyles.field} invalid={invalid}> <Label>Username</Label> <Input placeholder="Enter username" value={value} onValueChange={setValue} /> {invalid && <FieldError match>Username must be at least 3 characters</FieldError>} </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Fieldset
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { FloppyDisk } from "@gravity-ui/icons";import { Button, Description, FieldError, FieldGroup, Fieldset, Form, Input, Label, TextArea, TextField,} from "@lenso/ui";import { useId, type FormEvent } from "react";import { demoStyles } from "../../demo.stylex";
export function Basic() { const descriptionId = useId(); function onSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); alert("Form submitted successfully!"); } return ( <Form xstyle={demoStyles.wideColumn} onSubmit={onSubmit}> <Fieldset aria-describedby={descriptionId}> <Fieldset.Legend>Profile Settings</Fieldset.Legend> <Description id={descriptionId}>Update your profile information.</Description> <FieldGroup> <TextField name="name" validate={(value) => String(value).length < 3 ? "Name must be at least 3 characters" : null } > <Label required>Name</Label> <Input required placeholder="John Doe" /> <FieldError /> </TextField> <TextField name="email"> <Label required>Email</Label> <Input required type="email" placeholder="[email protected]" /> <FieldError /> </TextField> <TextField name="bio" validate={(value) => String(value).length < 10 ? "Bio must be at least 10 characters" : null } > <Label required>Bio</Label> <TextArea required placeholder="Tell us about yourself..." /> <Description>Minimum 10 characters</Description> <FieldError /> </TextField> </FieldGroup> <Fieldset.Actions> <Button type="submit"> <FloppyDisk aria-hidden="true" /> Save changes </Button> <Button type="reset" variant="secondary"> Cancel </Button> </Fieldset.Actions> </Fieldset> </Form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Form
"use client";// Adapted from HeroUI v3.2.6, e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e, Apache-2.0.import { Check } from "@gravity-ui/icons";import { Button, Description, FieldError, Form, Input, Label, TextField } from "@lenso/ui";import type { FormEvent } from "react";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ form: { display: "flex", flexDirection: "column", width: 384, maxWidth: "100%", gap: 16 }, actions: { display: "flex", gap: 8 },});
export function Basic() { function onSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget).entries()); alert(`Form submitted with: ${JSON.stringify(data, null, 2)}`); } return ( <Form xstyle={styles.form} onSubmit={onSubmit}> <TextField name="email" validate={(value) => /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(String(value)) ? null : "Please enter a valid email address" } > <Label required>Email</Label> <Input required type="email" placeholder="[email protected]" /> <FieldError /> </TextField> <TextField name="password" validate={(value) => { if (String(value).length < 8) return "Password must be at least 8 characters"; if (!/[A-Z]/.test(String(value))) return "Password must contain at least one uppercase letter"; if (!/[0-9]/.test(String(value))) return "Password must contain at least one number"; return null; }} > <Label required>Password</Label> <Input required minLength={8} type="password" placeholder="Enter your password" /> <Description>Must be at least 8 characters with 1 uppercase and 1 number</Description> <FieldError /> </TextField> <div {...stylex.props(styles.actions)}> <Button type="submit"> <Check aria-hidden="true" /> Submit </Button> <Button type="reset" variant="secondary"> Reset </Button> </div> </Form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Input
"use client";
import { Input } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
// HeroUI v3.2.6, e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e, Apache-2.0.const styles = stylex.create({ field: { width: 256 } });export function Basic() { return <Input aria-label="Name" xstyle={styles.field} placeholder="Enter your name" />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Label
"use client";// Adapted from HeroUI v3.2.6, Apache-2.0.import { Input, Label } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useId } from "react";import { demoStyles } from "../../demo.stylex";
export function Basic() { const id = useId(); return ( <div {...stylex.props(demoStyles.column)}> <Label htmlFor={id}>Name</Label> <Input xstyle={demoStyles.field} id={id} placeholder="Enter your name" type="text" /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
RadioGroup
export { Basic } from "./examples";Local adaptation source above. Derived from HeroUI v3.2.6 source.
TextField
"use client";
// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });
export function Basic() { return ( <TextField name="email" xstyle={styles.field}> <Label>Email</Label> <Input type="email" placeholder="Enter your email" /> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
TextArea
"use client";
// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { TextArea } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 384, height: 128 } });export function Basic() { return ( <TextArea aria-label="Quick project update" xstyle={styles.field} placeholder="Share a quick project update..." /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Form Field Tokens
Introduced form field tokens --field-* for consistent styling across form components. See Theming for the --field-* variables.
Storybook Organization
Reorganized Storybook by category for better navigation and component discovery.
Skeleton Animation Token
🚧 Breaking Changes: Renamed --skeleton-default-animation-type to --skeleton-animation in Skeleton for consistency with other component tokens.
Data-Slot Alignment
Aligned data-slot markers across components for consistent styling and customization. This standardization makes it easier to target specific component parts with CSS selectors and improves the overall developer experience when customizing component styles.
Components now use consistent data-slot attributes like:
data-slot="base"for the root elementdata-slot="label"for label textdata-slot="description"for description textdata-slot="error"for error messages
This allows for predictable CSS targeting across all form components:
.radio { [data-slot="label"] { /* Styles apply to radio labels */ }}Documentation Improvements
Component Documentation
- Link: Added Anatomy, and examples with Icon. Updated Link and Link.Icon props section.
- Description, FieldError, Fieldset, Form, Input, Label, RadioGroup, TextField, and TextArea: New documentation with usage examples
Migration Guide
Skeleton Component Migration
- Update animation token:
- Replace
--skeleton-default-animation-typewith--skeleton-animation
- Replace
Links
- GitHub PR #5780
- Description Component
- FieldError Component
- Fieldset Component
- Form Component
- Input Component
- Label Component
- RadioGroup Component
- TextField Component
- TextArea Component
- Skeleton Component
Contributors
Thanks to everyone who contributed to this release!