Skip to main content

generate-frontend-forms

Guide for creating forms using Sentry's new form system. Use when implementing forms, form fields, validation, or auto-save functionality.

Jump to install

Source facts

Repository
getsentry/sentry
Last source activity
August 28, 2026 at 08:09
Detected SKILL.md language
English
Stars
44,822
Forks
4,858

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
generate-frontend-forms
description
Guide for creating forms using Sentry's new form system. Use when implementing forms, form fields, validation, or auto-save functionality.
# Form System Guide This skill provides patterns for building forms using Sentry's new form system built on TanStack React Form and Zod validation. ## Core Principle - Always use the new form system (`useScrapsForm`, `AutoSaveForm`) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems. - All forms should be schema based. DO NOT create a form without schema validation. ## Imports All form components are exported from `@sentry/scraps/form`: ```tsx import {z} from 'zod'; import { AutoSaveForm, defaultFormOptions, setFieldErrors, useScrapsForm, } from '@sentry/scraps/form'; ``` > **Important**: DO NOT import from deeper paths, like '@sentry/scraps/form/field'. You can only use what is part of the PUBLIC interface in the index file in @sentry/scraps/form. --- ## Form Hook: `useScrapsForm` The main hook for creating forms with validation and submission handling. ### Basic Usage ```tsx import {z} from 'zod'; import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form'; const schema = z.object({ email: z.string().email('Invalid email'), name: z.string().min(2, 'Name must be at least 2 characters'), }); function MyForm() { const form = useScrapsForm({ ...defaultFormOptions, defaultValues: { email: '', name: '', }, validators: { onDynamic: schema, }, onSubmit: ({value, formApi}) => { // Handle submission console.log(value); }, }); return ( <form.AppForm form={form}> <form.AppField name="email"> {field => ( <field.Layout.Stack label="Email" required> <field.Input value={field.state.value} onChange={field.handleChange} /> </field.Layout.Stack> )} </form.AppField> <form.SubmitButton>Submit</form.SubmitButton> </form.AppForm> ); } ``` > **Important**: Always spread `defaultFormOptions` first. It configures validation to run on submit initially, then on every change after the first submission. This is why validators are defined as `onDynamic`, and it's what provides a consistent UX. ### Returned Properties | Property | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------- | | `AppForm` | Root wrapper component (provides form context and renders `<form>` element). Must receive `form={form}` prop. | | `AppField` | Field renderer component | | `FieldGroup` | Section grouping with title | | `SubmitButton` | Pre-wired submit button | | `Subscribe` | Subscribe to form state changes | | `reset()` | Reset form to default values | | `handleSubmit()` | Manually trigger submission | --- ## Field Components All fields are accessed via the `field` render prop and follow consistent patterns. ### Input Field (Text) ```tsx <form.AppField name="firstName"> {field => ( <field.Layout.Stack label="First Name" required> <field.Input value={field.state.value} onChange={field.handleChange} placeholder="Enter your name" /> </field.Layout.Stack> )} </form.AppField> ``` ### Number Field ```tsx <form.AppField name="age"> {field => ( <field.Layout.Stack label="Age" required> <field.Number value={field.state.value} onChange={field.handleChange} min={0} max={120} step={1} /> </field.Layout.Stack> )} </form.AppField> ``` ### Select Field (Single) ```tsx <form.AppField name="country"> {field => ( <field.Layout.Stack label="Country"> <field.Select value={field.state.value} onChange={field.handleChange} options={[ {value: 'us', label: 'United States'}, {value: 'uk', label: 'United Kingdom'}, ]} /> </field.Layout.Stack> )} </form.AppField> ``` ### Select Field (Multiple) ```tsx <form.AppField name="tags"> {field => ( <field.Layout.Stack label="Tags"> <field.Select multiple value={field.state.value} onChange={field.handleChange} options={[ {value: 'bug', label: 'Bug'}, {value: 'feature', label: 'Feature'}, ]} clearable /> </field.Layout.Stack> )} </form.AppField> ``` ### Switch Field (Boolean) ```tsx <form.AppField name="notifications"> {field => ( <field.Layout.Stack label="Enable notifications"> <field.Switch checked={field.state.value} onChange={field.handleChange} /> </field.Layout.Stack> )} </form.AppField> ``` ### TextArea Field ```tsx <form.AppField name="bio"> {field => ( <field.Layout.Stack label="Bio"> <field.TextArea value={field.state.value} onChange={field.handleChange} rows={4} placeholder="Tell us about yourself" /> </field.Layout.Stack> )} </form.AppField> ``` ### Range Field (Slider) ```tsx <form.AppField name="volume"> {field => ( <field.Layout.Stack label="Volume"> <field.Range value={field.state.value} onChange={field.handleChange} min={0} max={100} step={10} /> </field.Layout.Stack> )} </form.AppField> ``` ### Radio Field Radio fields use a composable API with `Radio.Group` and `Radio.Item`. `Radio.Group` provides group context that changes how the label is rendered for proper accessibility semantics. > **Important**: The layout (and its label) **must** be rendered _inside_ `Radio.Group`. The group context is provided by `Radio.Group`, so placing the layout outside will result in incorrect accessibility semantics. ```tsx <form.AppField name="priority"> {field => ( <field.Radio.Group value={field.state.value} onChange={field.handleChange}> <field.Layout.Stack label="Priority"> <field.Radio.Item value="low">Low</field.Radio.Item> <field.Radio.Item value="medium">Medium</field.Radio.Item> <field.Radio.Item value="high" description="Urgent issues"> High </field.Radio.Item> </field.Layout.Stack> </field.Radio.Group> )} </form.AppField> ``` For horizontal arrangement of radio items, use a `Flex` or `Stack` wrapper inside the layout: ```tsx import {Flex} from '@sentry/scraps/layout'; <field.Radio.Group value={field.state.value} onChange={field.handleChange}> <field.Layout.Row label="Priority"> <Flex gap="lg"> <field.Radio.Item value="low">Low</field.Radio.Item> <field.Radio.Item value="high">High</field.Radio.Item> </Flex> </field.Layout.Row> </field.Radio.Group>; ``` ### Custom Fields with BaseField For one-off fields that don't have a built-in component (e.g. a color picker, or any custom input), use `field.Base`. It provides a render prop with all the necessary accessibility and form integration props (`ref`, `disabled`, `aria-invalid`, `aria-describedby`, `onBlur`, `name`, `id`) that you spread onto your native element. ```tsx <form.AppField name="color"> {field => ( <field.Layout.Row label="Brand Color"> <field.Base<HTMLInputElement>> {(baseProps, {indicator}) => ( <Flex flexGrow={1}> <input {...baseProps} type="color" value={field.state.value} onChange={e => field.handleChange(e.target.value)} /> {indicator} </Flex> )} </field.Base> </field.Layout.Row> )} </form.AppField> ``` The render prop receives two arguments: 1. **`baseProps`** — accessibility and form integration props (`ref`, `disabled`, `aria-invalid`, `aria-describedby`, `onBlur`, `name`, `id`) to spread onto your element 2. **`{indicator}`** — the auto-save status indicator (spinner/checkmark) as a React node, which you can place wherever makes sense in your custom layout The element type is inferred from the passed `ref`, so if you don't pass one, you have to manually annotate it with `<field.Base<HTMLInputElement>>`. `field.Base` automatically handles: - Merging refs (for scroll-to-hash and external ref forwarding) - Disabling the field when auto-save is pending - Setting `aria-invalid` based on validation state - Linking to hint text via `aria-describedby` Use `field.Base` instead of building custom wrappers that duplicate this logic. It works with any native HTML element or third-party component that accepts standard props. --- ## Layouts Two layout options are available for positioning labels and fields. ### Stack Layout (Vertical) Label above, field below. Best for forms with longer labels or mobile layouts. ```tsx <field.Layout.Stack label="Email Address" hintText="We'll never share your email" required > <field.Input value={field.state.value} onChange={field.handleChange} /> </field.Layout.Stack> ``` ### Row Layout (Horizontal) Label on left (~50%), field on right. Compact layout for settings pages. ```tsx <field.Layout.Row label="Email Address" hintText="We'll never share your email" required> <field.Input value={field.state.value} onChange={field.handleChange} /> </field.Layout.Row> ``` ### Compact Variant Both Stack and Row layouts support a `variant="compact"` prop. In compact mode, the hint text appears as a tooltip on the label instead of being displayed below. This saves vertical space while still providing the hint information. ```tsx // Default: hint text appears below the label <field.Layout.Row label="Email" hintText="We'll never share your email"> <field.Input ... /> </field.Layout.Row> // Compact: hint text appears in tooltip when hovering the label <field.Layout.Row label="Email" hintText="We'll never share your email" variant="compact"> <field.Input ... /> </field.Layout.Row> // Also works with Stack layout <field.Layout.Stack label="Email" hintText="We'll never share your email" variant="compact"> <field.Input ... /> </field.Layout.Stack> ``` **When to Use Compact**: - Settings pages with many fields where vertical space is limited - Forms where hint text is supplementary, not essential - Dashboards or panels with constrained height ### Custom Layouts You are allowed to create new layouts if necessary, or not use any layouts at all. Without a layout, you _should_ render `field.meta.Label` and optionally `field.meta.HintText` for a11y. ```tsx <form.AppField name="firstName"> {field => ( <Flex gap="md"> <field.Meta.Label required>First Name:</field.Meta.Label> <field.Input value={field.state.value ?? ''} onChange={field.handleChange} /> </Flex> )} </form.AppField> ``` ### Layout Props | Prop | Type | Description |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub