bx

Form

Building forms with validation and accessible field markup.

This is your public display name.

tsx
<div className="w-full max-w-sm">
  <Form>
    <FormItem>
      <FormLabel for="u">Username</FormLabel>
      <FormControl><Input id="u" placeholder="shadcn" /></FormControl>
      <FormDescription>This is your public display name.</FormDescription>
    </FormItem>
    <div className="mt-4"><Button type="submit">Submit</Button></div>
  </Form>
</div>

Form provides layout helpers to build a form with consistent labels, descriptions and error messages. It does not validate (do that on the server or on the client, depending on your stack).

Structure: Form (wrapper) > FormItem (one row) > FormLabel + FormControl (field wrapper) + FormDescription (help text) + FormMessage (error or hint).

Installation

Terminal
$ bext ui add form

Usage

tsx
import { Form, FormItem, FormLabel, FormControl, FormDescription, Input, Button } from "@bext-stack/ui/primitives"

<Form action="/api/login" method="post">
  <FormItem>
    <FormLabel for="email">Email</FormLabel>
    <FormControl><Input id="email" name="email" type="email" required /></FormControl>
    <FormDescription>We'll never share your email.</FormDescription>
  </FormItem>
  <Button type="submit">Log in</Button>
</Form>

Anatomy

<Form action="/submit" method="post">
  └── <FormItem>
      ├── <FormLabel>Email</FormLabel>
      ├── <FormControl><Input /></FormControl>
      ├── <FormDescription>We'll never share it.</FormDescription>
      └── <FormMessage>Invalid email format</FormMessage>

API Reference

Form

Prop Type Default Description
action string — URL the form is submitted to. Omitted from the HTML when not set (the form then posts to the current URL).
method "get" | "post" "POST" HTTP method of the <form>.
enctype string — Encoding, for example "multipart/form-data" for file uploads.
children ReactNode — FormItems and the submit button, stacked with gap-6.

FormItem

Prop Type Default Description
children ReactNode — One field: label, control, description, message. grid gap-2 container.

FormLabel

Prop Type Default Description
for string — id of the input the label names. The prop is "for": "htmlFor" is not read.
error boolean false Sets data-error="true", which turns the label red.
children ReactNode — Label text.

FormControl

Prop Type Default Description
children ReactNode — Direct wrapper of the <Input>/<Select>/<Textarea>.
id string — id of the wrapper <div> (not of the input).
describedBy string — Space-separated ids of the description and message. Written as aria-describedby on the field inside the wrapper (the first input, select or textarea), added to any value the field already has.
invalid boolean false Sets aria-invalid="true" on the field inside the wrapper.

FormDescription

Prop Type Default Description
id string — id of the <p>, to reference from FormControl's describedBy.
children ReactNode — text-sm text-muted-foreground.

FormMessage

Prop Type Default Description
id string — id of the <p>, to reference from FormControl's describedBy.
error boolean true When true the message is role="alert". Pass false for a hint that should not be announced as an error.
children ReactNode — Error text, text-sm text-destructive. Renders nothing when it has no children.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-slot every sub-component "form" | "form-item" | "form-label" | "form-control" | "form-description" | "form-message" Stable selector for custom styling.
data-error FormLabel "true" (absent otherwise) Present when the error prop is set.

Accessibility

Link each label to its input: <FormLabel for="id"> with the same id on the Input. The components do not generate ids.

FormControl writes aria-describedby (from describedBy) and aria-invalid="true" (from invalid) on the field itself, not on its wrapper. The field is the first <input> (hidden inputs are skipped), <select> or <textarea> in its children, or failing that a <button role="combobox">. An aria-describedby already on the field is kept and the new ids are appended. If the children contain no field, the two attributes stay on the wrapper <div>.

Give FormDescription and FormMessage an id and list those ids in describedBy. The components do not generate or link ids for you. FormMessage is role="alert", so an error rendered after a submit is announced; pass error={false} for a neutral hint.

Examples

Field in error

We only use it to send the receipt.

tsx
// aria-describedby and aria-invalid end up on the <input>, not on the wrapper.
<Form>
  <FormItem>
    <FormLabel for="form-email" error>Email</FormLabel>
    <FormControl describedBy="form-email-help form-email-error" invalid>
      <Input id="form-email" type="email" value="ada@" />
    </FormControl>
    <FormDescription id="form-email-help">We only use it to send the receipt.</FormDescription>
    <FormMessage id="form-email-error">Enter a complete email address.</FormMessage>
  </FormItem>
</Form>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.