ProductBrand
Join us
Foundations
  • Introduction
  • Colors
  • Typography
  • Icons
  • Illustrations
  • Logos
  • Shadows
Components
  • Accordion
  • Alert Dialog
  • Avatar
  • Badge
  • Button
  • Calendar
  • Card
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Context Menu
  • Date Picker
  • Dialog
  • Drawer
  • Dropdown Menu
  • Fluid Avatar
  • Hover Scroll Text
  • Input
  • Kbd
  • Popover
  • Progress Bar
  • Radio Group
  • Scroll Area
  • Select
  • Selectors
  • Separator
  • Separator Dot
  • Shimmer Text
  • Skeleton
  • Slider
  • Switch
  • Tabs
  • Textarea
  • Toast
  • Tooltip
Compositions
  • Empty State
  • File Attachment
  • Floating Bar
  • Input Banner
  • Markdown Editor
  • Media Controls
  • Product Feature Banner
  • Panels
  • Sidebar
  • Reasoning Trace
  • Pagination
  • Data Table
  • Breadcrumb
  • Thread Outline
Inline
  • Overview
  • Stat Tile
  • Metric Row
  • Comparison Table
  • Chart Card
  • Trend Tile
  • Cite
  • Custom
  • Agent skill

Form

Form primitives connect react-hook-form state to labels, descriptions, and inline error messages, and wire aria-invalid / aria-describedby for you. Validate with a zod schema at module scope so the rules are testable without rendering anything.

Import

tsx
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@rogo-technologies/ui/form";
import { zodFormResolver } from "@rogo-technologies/ui/zod-form-resolver";

react-hook-form, @hookform/resolvers, and zod are optional peer dependencies, so install them in the consuming app (pinned to 7.71.0 / 5.4.0 / 4.4.3).

Examples

Validated form

Who to contact about membership changes.

Submit the form empty to see each FormMessage render its zod message, with the offending control marked aria-invalid and pointed at the message via aria-describedby.

Usage

tsx
const TeamSchema = z.object({
  name: z.string().trim().min(2, "Use at least 2 characters so the team is searchable."),
});

type TeamForm = z.infer<typeof TeamSchema>;

function CreateTeamForm() {
  const form = useForm<TeamForm>({
    resolver: zodFormResolver<TeamForm>(TeamSchema),
    defaultValues: { name: "" },
  });

  return (
    <Form {...form}>
      <form id="create-team" noValidate onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Team name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
      </form>
      <Button type="submit" form="create-team">
        Save
      </Button>
    </Form>
  );
}

Parts

PartRendersNotes
Form-FormProvider from react-hook-form. Spread the useForm return into it.
FormField-Controller plus the field-name context the other parts read.
FormItemdivGenerates the id shared by label, control, description, and message.
FormLabellabelhtmlFor the control; turns destructive when the field has an error.
FormControlslotClones its child, adding id, aria-invalid, and aria-describedby. One child only.
FormDescriptionpHelp text, always referenced by aria-describedby.
FormMessagepThe field's error message, or its children when there is no error. Renders nothing when both are empty.

useFormField() exposes the same ids and field state for a control that can't be wrapped in FormControl.

Guidelines

  • Keep the submit button outside <form> and target it with form="<id>" when the footer is sticky and the fields scroll, since a button inside the scroll container scrolls away with them.
  • Search and filter inputs inside a form get form="" so pressing Enter in them can't submit the enclosing form.
  • reset() in an effect when the query lands, not in defaultValues, when the form is seeded from server data, since defaultValues is captured once and a late-arriving response would otherwise leave the fields empty.
  • noValidate on the <form> so zod owns the messages rather than the browser's native bubbles.

Design at Rogo

We’re redesigning an entire industry. Come design it with us.

See open roles

Design at Rogo

  • How we workThe mission of design at Rogo, and the open roles
  • About RogoThe company and the product