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 @heroui/styles@alpha @heroui/react@alphapnpm add @heroui/styles@alpha @heroui/react@alphayarn add @heroui/styles@alpha @heroui/react@alphabun add @heroui/styles@alpha @heroui/react@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";
import {Description, Input, Label} from "@heroui/react";
export function Basic() {
  return (
    <div className="flex flex-col gap-1">
      <Label htmlFor="email">Email</Label>
      <Input
        aria-describedby="email-description"
        className="w-64"
        id="email"
        placeholder="you@example.com"
        type="email"
      />
      <Description id="email-description">
        We'll never share your email with anyone else.
      </Description>
    </div>
  );
}FieldError
"use client";
import {FieldError, Input, Label, TextField} from "@heroui/react";
import {useState} from "react";
export function Basic() {
  const [value, setValue] = useState("");
  const isInvalid = value.length > 0 && value.length < 3;
  return (
    <TextField className="w-64" isInvalid={isInvalid}>
      <Label htmlFor="username">Username</Label>
      <Input
        id="username"
        placeholder="Enter username"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <FieldError>Username must be at least 3 characters</FieldError>
    </TextField>
  );
}Fieldset
"use client";
import {
  Button,
  Description,
  FieldError,
  FieldGroup,
  Fieldset,
  Form,
  Input,
  Label,
  TextArea,
  TextField,
} from "@heroui/react";
import {Icon} from "@iconify/react";
export function Basic() {
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const data: Record<string, string> = {};
    // Convert FormData to plain object
    formData.forEach((value, key) => {
      data[key] = value.toString();
    });
    alert("Form submitted successfully!");
  };
  return (
    <Form className="w-full max-w-96" onSubmit={onSubmit}>
      <Fieldset.Root>
        <Fieldset.Legend>Profile Settings</Fieldset.Legend>
        <Description>Update your profile information.</Description>
        <FieldGroup>
          <TextField
            isRequired
            name="name"
            validate={(value) => {
              if (value.length < 3) {
                return "Name must be at least 3 characters";
              }
              return null;
            }}
          >
            <Label>Name</Label>
            <Input placeholder="John Doe" />
            <FieldError />
          </TextField>
          <TextField isRequired name="email" type="email">
            <Label>Email</Label>
            <Input placeholder="john@example.com" />
            <FieldError />
          </TextField>
          <TextField
            isRequired
            name="bio"
            validate={(value) => {
              if (value.length < 10) {
                return "Bio must be at least 10 characters";
              }
              return null;
            }}
          >
            <Label>Bio</Label>
            <TextArea placeholder="Tell us about yourself..." />
            <Description>Minimum 10 characters</Description>
            <FieldError />
          </TextField>
        </FieldGroup>
        <Fieldset.Actions>
          <Button type="submit">
            <Icon icon="gravity-ui:floppy-disk" />
            Save changes
          </Button>
          <Button type="reset" variant="secondary">
            Cancel
          </Button>
        </Fieldset.Actions>
      </Fieldset.Root>
    </Form>
  );
}Form
"use client";
import {Button, Description, FieldError, Form, Input, Label, TextField} from "@heroui/react";
import {Icon} from "@iconify/react";
export function Basic() {
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const data: Record<string, string> = {};
    // Convert FormData to plain object
    formData.forEach((value, key) => {
      data[key] = value.toString();
    });
    alert(`Form submitted with: ${JSON.stringify(data, null, 2)}`);
  };
  return (
    <Form className="flex w-96 flex-col gap-4" onSubmit={onSubmit}>
      <TextField
        isRequired
        name="email"
        type="email"
        validate={(value) => {
          if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
            return "Please enter a valid email address";
          }
          return null;
        }}
      >
        <Label>Email</Label>
        <Input placeholder="john@example.com" />
        <FieldError />
      </TextField>
      <TextField
        isRequired
        minLength={8}
        name="password"
        type="password"
        validate={(value) => {
          if (value.length < 8) {
            return "Password must be at least 8 characters";
          }
          if (!/[A-Z]/.test(value)) {
            return "Password must contain at least one uppercase letter";
          }
          if (!/[0-9]/.test(value)) {
            return "Password must contain at least one number";
          }
          return null;
        }}
      >
        <Label>Password</Label>
        <Input placeholder="Enter your password" />
        <Description>Must be at least 8 characters with 1 uppercase and 1 number</Description>
        <FieldError />
      </TextField>
      <div className="flex gap-2">
        <Button type="submit">
          <Icon icon="gravity-ui:check" />
          Submit
        </Button>
        <Button type="reset" variant="secondary">
          Reset
        </Button>
      </div>
    </Form>
  );
}Input
"use client";
import {Input} from "@heroui/react";
export function Basic() {
  return <Input aria-label="Name" className="w-64" placeholder="Enter your name" />;
}Label
"use client";
import {Input, Label} from "@heroui/react";
export function Basic() {
  return (
    <div className="flex flex-col gap-1">
      <Label htmlFor="name">Name</Label>
      <Input className="w-64" id="name" placeholder="Enter your name" type="text" />
    </div>
  );
}RadioGroup
"use client";
import {Description, Label, Radio, RadioGroup} from "@heroui/react";
export function Basic() {
  return (
    <RadioGroup defaultValue="premium" name="plan">
      <Label>Plan selection</Label>
      <Description>Choose the plan that suits you best</Description>
      <Radio.Root value="basic">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Basic Plan</Label>
          <Description>Includes 100 messages per month</Description>
        </Radio.Content>
      </Radio.Root>
      <Radio.Root value="premium">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Premium Plan</Label>
          <Description>Includes 200 messages per month</Description>
        </Radio.Content>
      </Radio.Root>
      <Radio.Root value="business">
        <Radio.Control>
          <Radio.Indicator />
        </Radio.Control>
        <Radio.Content>
          <Label>Business Plan</Label>
          <Description>Unlimited messages</Description>
        </Radio.Content>
      </Radio.Root>
    </RadioGroup>
  );
}TextField
"use client";
import {Input, Label, TextField} from "@heroui/react";
export function Basic() {
  return (
    <TextField className="w-full max-w-64" name="email" type="email">
      <Label>Email</Label>
      <Input placeholder="Enter your email" />
    </TextField>
  );
}TextArea
"use client";
import {TextArea} from "@heroui/react";
export function Basic() {
  return (
    <TextArea
      aria-label="Quick project update"
      className="h-32 w-96"
      placeholder="Share a quick project update..."
    />
  );
}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
- 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!