apps/design-system/content/docs/ui-patterns/forms.mdx
Forms in Supabase Studio should follow consistent patterns to ensure a cohesive user experience across settings pages and side panels. This guide covers the most common form patterns and field types.
Forms in page layouts typically use PageSection components with Card containers. Fields use FormItemLayout with layout="flex-row-reverse" for horizontal alignment.
<ComponentPreview name="form-patterns-pagelayout" description="Complete form example with all field types in a PageLayout pattern" peekCode wide />
Forms in side panels (Sheets) use FormItemLayout with layout="horizontal" on wider panels and layout="vertical" on panels with a size of sm or below. The form is typically wrapped in a Sheet component.
<ComponentPreview name="form-patterns-sidepanel" description="Complete form example with all field types in a SidePanel/Sheet pattern" peekCode wide />
The form previews above include both repeated-field patterns used across Studio:
Use the shared Single Value Field Array fragment when each row is one text input managed by react-hook-form.
Use the shared Key/Value Field Array fragment when each row is two text inputs managed by react-hook-form.
Keep repeated-row validation in the form schema or shared validation helper, not in the fragment component itself.
Build a custom row when the cells are mixed controls, such as an input paired with a Select.
Always use FormItemLayout: Use FormItemLayout instead of manually composing FormItem, FormLabel, FormMessage, and FormDescription.
Layout selection:
layout="flex-row-reverse" for page layouts (horizontal alignment)layout="horizontal" for side panels with more widthlayout="vertical" for side panels with limited widthWrap inputs in FormControlShadcn: Always wrap form inputs with FormControl_Shadcn_ to ensure proper form integration.
Use Cards for grouping: Wrap form sections in Card components with CardContent and CardFooter for actions.
Handle dirty state: Show cancel buttons and disable save buttons based on form.formState.isDirty. Make sure you destructure isDirty from form.formState (see https://react-hook-form.com/docs/useform/formstate)
Error handling: Always use mutations with onSuccess and onError callbacks that show toast notifications.
Loading states: Show loading states on submit buttons using the loading prop.
Form IDs: When submit buttons are outside the form, use a form ID and reference it with the form prop on the button.