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 @heroui/styles@alpha @heroui/react@alpha
pnpm add @heroui/styles@alpha @heroui/react@alpha
yarn add @heroui/styles@alpha @heroui/react@alpha
bun add @heroui/styles@alpha @heroui/react@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

We'll never share your email with anyone else.
"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

Profile SettingsUpdate your profile information.
Minimum 10 characters
"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

Must be at least 8 characters with 1 uppercase and 1 number
"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

Plan selectionChoose the plan that suits you best
"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

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!