Components · Inputs
Form
BetaField layout, validation and error messages.
- Source
components/ui/form.tsx - shadcn/ui Form
- Figma: not linked yet (Q13)
Submit the empty form to see the errors, then fill it in.
When to use
- For any set of fields that is validated and saved together: a new client, a meal, a package, a check-in template.
- When fields need help text and error messages tied to them for screen readers.
When not to use
- For a single search or filter field that applies as the coach types: use the control on its own.
- For settings that save as soon as they change: use Switches without a submit button.
- For long tasks with several steps (onboarding, building a program): split them into steps or pages, each one a form.
Anatomy
- 1. Label (FormLabel): linked to the control with htmlFor
- 2. Control (FormControl): gets the id, aria-describedby and aria-invalid
- 3. Description (FormDescription): help text, always visible
- 4. Message (FormMessage): the error, shown only when the field is invalid
Form is a set of wrappers around react-hook-form. Each field is a FormField (the field's name and how to render it) containing a FormItem with the four parts above. Validation rules live in a zod schema passed to zodResolver. FormControl wraps any control: an Input, a Select trigger, a Checkbox, or a Date picker trigger, as in the example.
Variants
One layout: label above the control, help text below it, the error below that. Fields stack in one column with gap-6 between them.
- Two columns: short related fields (first and last name, weight and height) can sit side by side from the
smbreakpoint up. Keep one column on phones. - In a dialog or sheet: the same parts; the submit button goes in the footer. See Dialog.
States
| State | What changes | Token |
|---|---|---|
| Invalid | Label and error turn red; the control gets a red border and ring | text-destructive-text, border-destructive, ring-destructive/20 |
| Description | Muted help text | text-muted-foreground |
| Disabled field | The control at half opacity; keep the description to say why | opacity-(--opacity-disabled) |
| Submitting | Submit button disabled with a spinner and aria-busy | animate-spin, opacity-(--opacity-disabled) |
| Submitted | A status message after the button | text-success-text |
All error text uses destructive-text (red-700, 6.5:1 on white), in the form and in Input and Select alike. Upstream shadcn used text-destructive for form labels and messages; it is changed so one token is used for all error text.
Usage guidelines
- Required fields: most fields in coach-admin are required, so mark the optional ones with "(optional)" in the label and say so once at the top of the form. Don't use asterisks.
- When to validate: on submit, then again when the coach leaves a field they touched (
mode: "onTouched"). Don't show errors while the coach is still typing the first time. - On submit with errors: react-hook-form moves focus to the first invalid field, so its label and error are read. Don't disable the submit button to prevent errors; let coaches submit and see what's missing.
- While saving: disable the submit button, show a spinner and change the label ("Adding client…"). Keep the fields as they are.
- After saving: confirm with a toast or a status message, and move on (close the dialog, go to the client's page).
Content guidelines
- Labels: short nouns, sentence case: "Full name", "Program start date". Optional fields end with "(optional)" / "(اختياري)".
- Descriptions: what the value is used for, in one sentence: "We send the app invite to this address."
- Errors say how to fix the problem, not that there is one: "Enter an email like name@example.com", not "Invalid email". Keep the same message for the same rule everywhere.
- The submit button names the result: "Add client", "Save meal". Not "Submit".
- Arabic errors use the polite imperative ("أدخل", "اختر"), as in the example.
Accessibility
| Key | Does |
|---|---|
| Tab / Shift+Tab | Moves through the fields in reading order |
| Enter (in a text field) | Submits the form |
| Space, Enter (on the submit button) | Submits the form |
FormLabelsetshtmlForto the control's id.FormControlsets the control'sid,aria-describedby(the description, plus the message when there is an error) andaria-invalid.- A screen reader announces an invalid field as "Email, invalid entry, edit text, We send the app invite to this address. Enter an email like name@example.com."
- Errors are not a live region. They are announced when focus reaches the field: react-hook-form focuses the first invalid field on submit (
shouldFocusError, on by default). For forms with many fields, add an error summary at the top that links to each field. - The success message uses
role="status", so it is announced without moving focus. - Add
noValidateto the form so the browser's own bubbles don't show alongside these messages, and use the righttype,autoCompleteandinputModeon each input.
RTL
- Labels, help text and errors align to the start, on the right.
- Emails and phone numbers are typed left to right: set
dir="ltr"on those inputs. - Error messages are written in Arabic for Arabic screens. Build the zod schema with the current language's messages, as in the example, not with a fixed English string.
- Two-column layouts swap columns automatically because they use the grid's reading order.
Tokens used
Read from components/ui/form.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --destructive-text | text-destructive-text | red-700 | red-700 |
| --muted-foreground | text-muted-foreground | neutral-600 | neutral-600 |
- Type
text-sm
Props
<FormField>
No props of its own.
<FormItem>
Also takes every prop of <div>
No props of its own.
<FormLabel>
Also takes every prop of Radix Label.Root
No props of its own.
<FormControl>
Also takes every prop of <Slot.Root>
No props of its own.
<FormDescription>
Also takes every prop of <p>
No props of its own.
<FormMessage>
Also takes every prop of <p>
No props of its own.
Replaces in coach-admin
From the read-only audit of coach-admin: 26 style values, 35 uses.
coach-admin wraps fields in a form-group component with its own error text (error-text, invalid-feedback). FormItem, FormMessage and FormDescription replace them.
Classes and selectors
.form-group12.error-text4.post-form__invalid-feedback2.el-form-group:has(.text-input-widget)2.error-text--file1.form-group:first-child1.el-form-group1
Files
src/reusableComponents/formComponents/form-group/form-group.scss12
The inventory lists the first 12 locations of each value, so file counts are a lower bound.
Related
Input, Select, Checkbox, Date picker, Combobox, Label, Toast.