Skip to content
Lenso UI

v3.0.0-alpha.34

Essentials for building forms with a clean API Form, TextField, RadioGroup, Label, Input, Fieldset and more.

October 15, 2025

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@alpha

Using 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 element
  • data-slot="label" for label text
  • data-slot="description" for description text
  • data-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

Migration Guide

Skeleton Component Migration

  1. Update animation token:
    • Replace --skeleton-default-animation-type with --skeleton-animation

Contributors

Thanks to everyone who contributed to this release!