Skip to main content

Wrapper component for form validation and submission handling

Import

import { Form } from "heroui-solid";

Usage

Must be at least 8 characters with 1 uppercase and 1 number
import { Check } from "gravity-icons-solid"
import {
  Button,
  Description,
  FieldError,
  Form,
  Input,
  Label,
  TextField
} from "heroui-solid"
import { createSignal } from "solid-js"

export function Basic() {
  const [email, setEmail] = createSignal("")
  const [password, setPassword] = createSignal("")

  const validateEmail = (value: string) => {
    if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
      return "Please enter a valid email address"
    }
    return null
  }

  const validatePassword = (value: string) => {
    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
  }

  const emailError = () => (email() ? validateEmail(email()) : null)
  const passwordError = () => (password() ? validatePassword(password()) : null)

  const onSubmit = (e: SubmitEvent) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget as HTMLFormElement)
    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 class="flex w-96 flex-col gap-4" onSubmit={onSubmit}>
      <TextField
        isInvalid={!!emailError()}
        isRequired
        name="email"
        onChange={setEmail}
        value={email()}
      >
        <Label>Email</Label>
        <Input placeholder="john@example.com" type="email" />
        <FieldError>{emailError()}</FieldError>
      </TextField>

      <TextField
        isInvalid={!!passwordError()}
        isRequired
        name="password"
        onChange={setPassword}
        value={password()}
      >
        <Label>Password</Label>
        <Input
          minLength={8}
          placeholder="Enter your password"
          type="password"
        />
        <Description>
          Must be at least 8 characters with 1 uppercase and 1 number
        </Description>
        <FieldError>{passwordError()}</FieldError>
      </TextField>

      <div class="flex gap-2">
        <Button type="submit">
          <Check />
          Submit
        </Button>
        <Button type="reset" variant="secondary">
          Reset
        </Button>
      </div>
    </Form>
  )
}

Anatomy

Import all parts and piece them together.

import { Form, Button } from "heroui-solid";

export default () => (
  <Form>
    {/* Form fields go here */}
    <Button type="submit" />
    <Button type="reset" />
  </Form>
);

Styling

Passing Tailwind CSS classes

import { Form, TextField, Label, Input, FieldError, Button } from "heroui-solid";

function CustomForm() {
  return (
    <Form class="w-full max-w-md space-y-4 rounded-lg border border-border bg-surface-secondary p-6">
      <TextField>
        <Label class="text-sm font-medium">Email</Label>
        <Input class="rounded-full border-border/60" placeholder="Enter your email" />
        <FieldError />
      </TextField>
      <Button type="submit" class="w-full">
        Submit
      </Button>
    </Form>
  );
}

API Reference

Form Props

The Form component renders a native <form> element and forwards all of its attributes. It accepts every prop of an HTML <form>, including:

PropTypeDefaultDescription
actionstring-The URL to submit the form data to
method'get' | 'post'-The HTTP method to use when submitting the form
encTypestring-The encoding type for form data submission
target'_self' | '_blank' | '_parent' | '_top'-Where to display the response after submitting
onSubmit(event: SubmitEvent) => void-Handler called when the form is submitted
onReset(event: Event) => void-Handler called when the form is reset
classstring-Tailwind CSS classes applied to the form element
childrenJSX.Element-Form content (fields, buttons, etc.)

Form Validation

Unlike HeroUI React (which wires React Aria's validation system into the form), the Solid port renders a plain <form>. Drive validation yourself:

  • Use native HTML5 validation attributes on Input (required, minLength, pattern, …)
  • Compute each field's isInvalid from your own signals
  • Display the message through a FieldError child (see the Usage example)

Validation Behavior

There is no validationBehavior prop. Native HTML validation still applies when you set attributes like required on the Input; realtime, ARIA-style messages are produced by driving isInvalid and FieldError from state as the user types.

Form Submission

Forms can be submitted in several ways:

  • Traditional submission: set the action prop to submit to a URL
  • JavaScript handling: use the onSubmit handler to process form data
  • FormData API: access form data using the FormData API in your submit handler

Example with FormData:

function handleSubmit(e: SubmitEvent) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget as HTMLFormElement);
  const data = Object.fromEntries(formData);
  console.log("Form data:", data);
}

Integration with Form Fields

The Form component works with HeroUI's form field components:

  • TextField: for text inputs with labels and validation
  • Input / TextArea: the field controls themselves
  • Label / Description / FieldError: field satellites
  • Button: for form submission and reset actions

Place them inside the <Form> and they submit as part of its FormData.

Accessibility

  • Native <form> element semantics
  • Form landmark creation with aria-label or aria-labelledby
  • Labels associate with their controls through TextField
  • Field errors are announced through FieldError

Advanced Usage

For richer flows — server-side validation, form-context providers, or integration with a third-party form library — compose those around the plain <form>; the Form component intentionally stays a thin native wrapper.

Last updated: 7/19/26, 3:27 AM

HeroUI SolidUnofficial SolidJS port of HeroUI v3, built on Kobalte and @heroui/styles