On this page
Form wraps a set of inputs and actions, provides error context to fields, and manages error display and focus.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- When collecting multiple pieces of information from a user in a single submission flow
- When you need form-level error handling: an error summary listing field-level validation errors, or a general error for non-field failures such as an API timeout
- When fields need to participate in automatic error context so each field can read its own error state by name
When not to use
- For a single standalone input with no submission flow - a Text Field or other field component works without a Form wrapper.
- For read-only data display - use Summary List instead.
- For a list of read-only key-value pairs - use Summary List instead.
Anatomy
- Required fields notice ("Fields marked * are required", shown when the markRequiredFormFields theme option is enabled and hasMandatoryFields is true)
- Error summary (lists all field-level validation errors with links to each field; always rendered, visible only when errors is non-empty)
- General error (full-width error notification for non-field errors such as API failures; always rendered, visible only when generalError is set)
- Form fields (the FormFields container, which spaces direct children vertically)
- Form actions (the FormActions container, which places buttons in a horizontal row)
Basic usage
Edit live code
<Form> <FormFields> <TextField name="first-name" id="first-name" label="First name" /> <TextField name="last-name" id="last-name" label="Last name" /> <Checkbox name="terms" label="I agree to the terms & conditions" /> </FormFields> <FormActions> <PrimaryCTAButton>Submit</PrimaryCTAButton> <SecondaryCTAButton>Clear</SecondaryCTAButton> </FormActions> </Form>
Variants and features
Error summary
Pass an array of { id, message } objects to the errors prop. The ErrorSummaryNotification is always present in the DOM. When errors is non-empty it becomes visible and keyboard focus moves to it automatically.
Each id in the errors array must match the id of the corresponding field so that the error summary links correctly to the field. Nebula field components generate their own id, so pass the same name value to both the field and the error entry.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [errors, setErrors] = useState([]); const onSubmit = (event) => { event.preventDefault(); setErrors([ { id: 'summary-first-name', message: 'Enter your first name' }, { id: 'summary-last-name', message: 'Enter your last name' }, ]); }; return ( <Form errors={errors} onSubmit={onSubmit} noValidate> <FormFields> <TextField name="summary-first-name" id="summary-first-name" label="First name" /> <TextField name="summary-last-name" id="summary-last-name" label="Last name" /> </FormFields> <FormActions> <PrimaryCTAButton type="submit">Show error summary</PrimaryCTAButton> </FormActions> </Form> ); } return <Example />; }
General error
Use generalError for errors that are not tied to a specific field, such as an API timeout or a server error. Pass either title, message, or both. When generalError is set, a full-width error Notification appears below the error summary and focus moves to it.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [generalError, setGeneralError] = useState(undefined); const onSubmit = (event) => { event.preventDefault(); setGeneralError({ title: 'We could not save your details', message: 'The service did not respond. Try again in a few minutes.', }); }; return ( <Form generalError={generalError} onSubmit={onSubmit} noValidate> <FormFields> <TextField name="general-first-name" id="general-first-name" label="First name" /> </FormFields> <FormActions> <PrimaryCTAButton type="submit">Show general error</PrimaryCTAButton> </FormActions> </Form> ); } return <Example />; }
Required fields notice
When the markRequiredFormFields theme option is set to true, and the form contains at least one required field, the text "Fields marked * are required" appears at the top of the form. Set hasMandatoryFields={false} on forms where no fields are required to suppress this notice.
Edit live code
() => { // ignore - for live code example only. const customTheme = createTheme({ config: { markRequiredFormFields: true, }, }); return ( <ThemeProvider theme={customTheme}> <Form> <FormFields> <TextField id="required-first-name" label="First name" /> <TextField id="required-middle-name" label="Middle name" optional /> <TextField id="required-last-name" label="Last name" /> </FormFields> <FormActions> <PrimaryCTAButton>Submit</PrimaryCTAButton> </FormActions> </Form> </ThemeProvider> ) }
FormFields
FormFields wraps form inputs and applies consistent vertical spacing between them using the betweenFormFields spacing token. Use it as the direct parent of all field components inside a form.
Direct children are spaced using the token, including non-field elements:
Edit live code
<FormFields> <TextField name="spacing-first-name" id="spacing-first-name" label="First name" /> <TextField name="spacing-last-name" id="spacing-last-name" label="Last name" /> <div> <P>Example showing that a paragraph within a div receives the correct spacing.</P> </div> </FormFields>
A P placed directly inside FormFields brings its own margins, which conflict with the token spacing. Wrap it in a div instead, as above:
Edit live code
<FormFields> <TextField name="unspaced-first-name" id="unspaced-first-name" label="First name" /> <TextField name="unspaced-last-name" id="unspaced-last-name" label="Last name" /> <P>Example showing that a paragraph placed directly conflicts with intended spacing.</P> </FormFields>
FormActions
FormActions renders a row of buttons at the bottom of the form, using the betweenButtons spacing token. It applies a top margin of space[6] to separate it from the fields above.
The optional message prop renders a supplementary message above the button row, prefixed with an info icon. Use this for clarifying information that applies to the whole submission, such as a privacy notice or a note about processing time. Do not use it for validation errors.
Edit live code
<FormActions message={<>Once accepted, it can take up to <Strong>30 seconds</Strong> to update this account</>}> <PrimaryCTAButton>Submit</PrimaryCTAButton> <SecondaryCTAButton>Clear</SecondaryCTAButton> </FormActions>
Custom fields and useFormFieldError
Nebula's own field components read their error from the form context using the useFormFieldError hook. Pass the field's name to the hook and it returns the matching error message string, or undefined if there is no error.
Note: useFormFieldError is not exported from the Nebula public API. It is documented here to explain how the field components pick up errors from a surrounding Form, not as an API for application code. In application code, pass the errors array to Form and let the Nebula field components resolve their own errors by name, as shown in Error summary.
// Internal to Nebula - not importable from '@kaluza-platform/nebula'.
const errorMessage = useFormFieldError(name);
Common form features
Form fields share common behaviour for validation, hint text, required/optional marking, and field width. See the forms pattern guide for guidance on these topics.
usage
- If markRequiredFormFields theme option is trueForm must set hasMandatoryFields={false} when no fields in the form are requiredCopy rule ID for form.mandatory-fields-suppress-when-none-required
- Form should be used whenever collecting multiple pieces of user input in a single submission flowForm provides error context, error summary, and general error handling that field components rely on.Copy rule ID for form.use-for-multi-field-submission
- Form may be omitted when a page contains only a single standalone input with no submission flowField components function independently; Form is not a required wrapper for a single field.Copy rule ID for form.not-required-for-single-field
placement
- FormActions must be the last child of Form or FormFieldsSubmit and navigation actions follow the fields they act on. Placing actions before fields creates an illogical tab order.Copy rule ID for form.form-actions-last
content
- If markRequiredFormFields theme option is trueRequired fields notice must only appear when the form contains at least one required fieldShowing the notice on a form with no required fields misleads users.Copy rule ID for form.mandatory-fields-notice-accurate
- generalError must include at least one of title or messageAn empty object renders a visible but blank notification.Copy rule ID for form.general-error-title-or-message
- FormActions message must not be used to display validation errors or field-level guidanceThe message is rendered with an info icon and no error semantics. Validation errors belong in the errors prop or generalError.Copy rule ID for form.form-actions-message-not-for-errors
- FormActions message should contain only supplementary information that applies to the whole submission, such as a privacy notice or processing time noteCopy rule ID for form.form-actions-message-supplementary
interaction
- Form will set noValidate on the form element. Browser native validation is disabled automaticallyCopy rule ID for form.will-no-browser-validation
- ErrorSummaryNotification will receive keyboard focus automatically when it becomes visible. Do not suppress or override this behaviourMeets WCAG 2.2 SC 3.3.1 and 4.1.3.Copy rule ID for form.error-summary-focus
- General error Notification will receive keyboard focus automatically when generalError is set. Do not suppress or override this behaviourHandled via a useEffect in Form.Copy rule ID for form.general-error-focus
- Form onSubmit should be the point at which validation runs and the errors prop is updatedValidating on submit before updating errors ensures the error summary and field errors appear together, giving users a complete picture.Copy rule ID for form.submit-triggers-validation
validation
- errors array must use id values that match the id attribute rendered on the corresponding field elementThe error summary generates anchor links to each field using the id. A mismatch breaks the link and fails WCAG 2.2 SC 3.3.1.Copy rule ID for form.error-id-matches-field-id
composition
- ErrorSummaryNotification must always be present in the DOM, not conditionally mountedMounting the error summary only on error causes screen readers to miss focus movement. Always render it and let it show or hide based on the errors prop.Copy rule ID for form.errors-always-rendered
- general error Notification must always be present in the DOM, not conditionally mountedConditional mounting disrupts screen reader focus. Pass generalError to show it and undefined to hide it.Copy rule ID for form.general-error-always-rendered
- FormFields should be the direct parent of all field components inside a FormFormFields applies consistent betweenFormFields spacing. Nesting fields outside FormFields produces inconsistent vertical rhythm.Copy rule ID for form.form-fields-direct-parent
- useFormFieldError should be used by custom field components to read their error from form context rather than passing error strings as props manuallyKeeps error state in a single place and ensures custom fields participate in the same error summary flow as Nebula field components.Copy rule ID for form.custom-field-hook
Form
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
hasMandatoryFields | boolean | true | No | Shows a "Fields marked * are required" message above the form. Only has an effect when the theme's `markRequiredFormFields` option is on. |
errors | ErrorSummaryList | [] | No | Field errors. Shown in a summary at the top of the form, and passed to fields (matched by `name`) so they show their own error message. |
generalError | GeneralError | — | No | An error not tied to a single field, shown in an error notification at the top of the form and given focus when it appears. Provide a `title`, a `message`, or both. |
FormActions
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
message | ReactNode | — | No | Text shown above the buttons, for example to explain what will happen on submit. |
FormFields
Also accepts all props from HTMLAttributes.
This component has no props of its own.
Styles
mandatory-fields-heading
Defined in Form/Form.tsx
font-weight: ${fontWeights.normal};
font-family: ${fonts.body};
color: ${colors.body};
margin-bottom: ${space[6]};
font-size: ${responsiveFontSizes.body[0]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.body[1]};
}
span {
color: ${colors.error};
}
message-content
Defined in Form/FormActionMessage.tsx
font-family: ${fonts.body};
line-height: ${lineHeights.body};
font-weight: ${fontWeights.normal};
color: ${colors.body};
font-size: ${responsiveFontSizes.body[0]};
display: flex;
gap: ${space[1]};
> *:first-child {
flex-shrink: 0;
}
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.body[1]};
}
In brief
Form is a native form with the browser's own validation turned off. It lists field errors in a summary at the top, shows an error not tied to a field, and moves focus to either when it appears.
What Nebula provides
- A native form with noValidate, so the errors a user sees are Nebula's messages and not the browser's.
- An error summary of links, one per error, each going to the field whose id matches. It takes focus when errors arrive.
- An error that belongs to no field, from generalError, takes focus when it is set.
- Nebula fields inside the form show the error whose id matches their name. When the theme turns on markRequiredFormFields, the form shows "Fields marked * are required" above the fields.
Keyboard behaviour
The form is not a tab stop. When errors arrive, focus moves to the error summary, and when a generalError is set, it moves to that notification.
Accessible design
Say which fields are required before the first field
Nebula shows "Fields marked * are required" when the theme turns it on. Fields outside a Form do not get it, so write that exact sentence at the top yourself. The asterisk on each field is hidden from screen readers, which hear that the field is required instead.
WCAG: 3.3.2 Labels or Instructions
Write each error so it makes sense as a link on its own
The summary shows every message as link text, out of context, next to the others. Say what is wrong and how to fix it: "Enter a first name", not "Required".
WCAG: 3.3.1 Error Identification 3.3.3 Error Suggestion 2.4.4 Link Purpose (In Context)
Accessible development
Always give each error an id that matches its field's id
The summary link goes to #id, so a mismatch leaves a link that goes nowhere. The field's name has to match the same entry for the field to show its own error.
WCAG: 3.3.1 Error Identification
Keep the errors array the same between renders unless the errors change
Each new array moves focus to the error summary again, so an array written inline in the JSX takes focus from the user on every render.
If a page has more than one form, give each an accessible name
A form is exposed as a landmark only when it has a name, and a page with several forms needs to tell them apart. Form passes aria-label and aria-labelledby through to the element.
WCAG: 1.3.1 Info and Relationships
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Partially tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.