Form
Building forms with validation and accessible field markup.
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
$ bext ui add form
Copy and paste the following into src/components/ui/form.tsx.
Usage
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
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page