Skip to content
SuperCoach

Components · Inputs

Form

Beta

Field layout, validation and error messages.

  • Source components/ui/form.tsx
  • shadcn/ui Form
  • Figma: not linked yet (Q13)

All fields are required unless marked optional.

We send the app invite to this address.

Week 1 starts on this day.

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

We send the app invite to this address.

Enter an email like name@example.com.

  1. 1. Label (FormLabel): linked to the control with htmlFor
  2. 2. Control (FormControl): gets the id, aria-describedby and aria-invalid
  3. 3. Description (FormDescription): help text, always visible
  4. 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 sm breakpoint up. Keep one column on phones.
  • In a dialog or sheet: the same parts; the submit button goes in the footer. See Dialog.

States

Enter a weight between 30 and 250 kg.

Change it from the client's nutrition tab.

StateWhat changesToken
InvalidLabel and error turn red; the control gets a red border and ringtext-destructive-text, border-destructive, ring-destructive/20
DescriptionMuted help texttext-muted-foreground
Disabled fieldThe control at half opacity; keep the description to say whyopacity-(--opacity-disabled)
SubmittingSubmit button disabled with a spinner and aria-busyanimate-spin, opacity-(--opacity-disabled)
SubmittedA status message after the buttontext-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

Do.Every field is required unless its label says optional. One note at the top says so.
Don't.An asterisk on every field. It is easy to miss and screen readers read it as “star”.
  • 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

KeyDoes
Tab / Shift+TabMoves through the fields in reading order
Enter (in a text field)Submits the form
Space, Enter (on the submit button)Submits the form
  • FormLabel sets htmlFor to the control's id. FormControl sets the control's id, aria-describedby (the description, plus the message when there is an error) and aria-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 noValidate to the form so the browser's own bubbles don't show alongside these messages, and use the right type, autoComplete and inputMode on 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.

Semantic color tokens used by this component
TokenUtilitiesSuperCoachCoach
--destructive-texttext-destructive-textred-700red-700
--muted-foregroundtext-muted-foregroundneutral-600neutral-600
  • Typetext-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.

Input, Select, Checkbox, Date picker, Combobox, Label, Toast.