FormSection and actions
The building blocks of a page form - sections with a header, the action row, submit and cancel buttons with a pending state, and a confirmation.
A form on a page is built from a few pieces: Form groups related fields in a card with a
header, Form holds the buttons of the form as a whole, Submit and
Save show a pending state, Form asks before it discards changes,
and Form confirms the result. None of them knows a form library: your application binds
them.
A settings form with sections and actionsSimulationA server imitated in your browser: shows the interaction, not a working server integration.
// apps/docs/examples/ui/form-section.tsx'use client';
import {useState} from 'react';
import {SubmitButton} from '@workspace/active-ui/components/composed/buttons';import {FormActions} from '@workspace/active-ui/components/composed/form-actions';import {FormCancelButton} from '@workspace/active-ui/components/composed/form-cancel-button';import {FormSection} from '@workspace/active-ui/components/composed/form-section';import {FormSuccess} from '@workspace/active-ui/components/composed/form-success';import {Button} from '@workspace/active-ui/components/primitives/button';import { Field, FieldLabel,} from '@workspace/active-ui/components/primitives/field';import {Input} from '@workspace/active-ui/components/primitives/input';
const SAVED = {name: 'Northwind pension fund', email: 'office@example.com'};
/** * A settings form in two sections with the form's action row. Change a * field to make the form dirty: Cancel then asks before it discards. Saving * is simulated with a short wait; nothing is sent. */export default function FormSectionExample() { const [saved, setSaved] = useState(SAVED); const [values, setValues] = useState(SAVED); const [isPending, setIsPending] = useState(false); const [message, setMessage] = useState(''); const isDirty = values.name !== saved.name || values.email !== saved.email;
const save = (event: React.FormEvent) => { event.preventDefault(); setIsPending(true); setMessage(''); window.setTimeout(() => { setSaved(values); setIsPending(false); setMessage('Your changes are saved.'); }, 1200); };
return ( <form className="grid w-full max-w-xl gap-6" onSubmit={save}> <FormSection title="Organisation" description="How the organisation appears on statements." > <Field> <FieldLabel htmlFor="org-name">Name</FieldLabel> <Input id="org-name" value={values.name} onChange={(event) => setValues({...values, name: event.target.value}) } /> </Field> </FormSection> <FormSection title="Contact" actions={ <Button type="button" variant="outline" size="sm"> Add a contact </Button> } footer={ <p className="text-muted-foreground text-sm"> Last changed: 16 May 2026 </p> } variant="compact" > <Field> <FieldLabel htmlFor="org-email">Email</FieldLabel> <Input id="org-email" type="email" value={values.email} onChange={(event) => setValues({...values, email: event.target.value}) } /> </Field> </FormSection> <FormActions> <FormCancelButton label="Cancel" isDirty={isDirty} disabled={isPending} onConfirm={() => { setValues(saved); setMessage(''); }} dialog={{ title: 'Unsaved changes', description: 'You have unsaved changes. Discard them?', confirmLabel: 'Discard changes', cancelLabel: 'Keep editing', }} /> <SubmitButton isPending={isPending} normalLabel="Save" disabled={!isDirty} /> </FormActions> <FormSuccess message={message} /> </form> );}Usage#
import {SubmitButton} from '@workspace/active-ui/components/composed/buttons';import {FormActions} from '@workspace/active-ui/components/composed/form-actions';import {FormCancelButton} from '@workspace/active-ui/components/composed/form-cancel-button';import {FormSection} from '@workspace/active-ui/components/composed/form-section';
<form onSubmit={save}> <FormSection title="Organisation" description="How the organisation appears."> โฆfields </FormSection> <FormActions> <FormCancelButton label="Cancel" isDirty={isDirty} onConfirm={leave} dialog={{ title: 'Unsaved changes', description: 'You have unsaved changes. Discard them?', confirmLabel: 'Discard changes', cancelLabel: 'Keep editing', }} /> <SubmitButton isPending={isPending} normalLabel="Save" /> </FormActions></form>;The pieces#
Form: a card with anSection <h3>title, a description andactionson the title's line (an add button belongs there, not below the fields), and afooterrow aligned to the end. A description without a title is a muted paragraph.variant="compact"uses less padding. The page title belongs in a page header, one level up.Form: the same header on its own, for a section with its own frame.Section Header Form: the row of the form's actions, aligned to the start with a gap: cancel first, the primary action last. Not for dialog footers, which align to the end, nor for row or toolbar actions. It does not wrap.Actions Submit:Button type="submit". Whileisit shows a spinner, keeps its width (Pending min) and its name, and is disabled;Width Class disabledadds a reason of your own, such as a form without changes.Save: a second action with its ownAnd Continue Button onand the same pending state.Click Cancel: aButton type="button"in the secondary variant, so it never submits.Form: a cancel that leaves the page. A form without changes leaves at once; with changes, a ConfirmActionDialog asks first, with the confirm in the destructive variant. For a cancel that only closes something, useCancel Button Cancel.Button Form: a confirmation in a polite live region, centred; nothing renders without aSuccess message.
What your application provides#
The form binding, the dirty state, the pending state and what saving and leaving do.
Every visible text, translated: there are no English defaults.
Errors per field, with Field.
Accessibility and keyboard#
The sections are
<section>elements with an<h3>title, so the headings outline the form.A pending submit button keeps its accessible name behind the spinner and cannot be pressed twice.
Formmounts together with its message. Some screen readers skip a live region that arrives with its text; where the confirmation must be heard, keep an emptySuccess role="status"region in the page and fill it, or move focus to the result.
API#
FormSection#
Import from @workspace/
Props
- Type
ReactNode
The fields / content of the section.
- Type
ReactNode
Optional action(s) in the header row, to the right of title and description (requires title). For the add button of a list section: it belongs in the header, on the same line as the title, and not in the footer below the grid.
- Type
string
Custom CSS class on the section container.
- Type
string
Optional description below the title.
- Type
string
Optional section title; appears as an <h3> at the top.
- Type
FormSection Variant - Default
'default'
Padding variant. Default: "default" (p-6); "compact" (p-4).
Takes only the props listed.
Types
- Type
type FormSection Variant = 'default' | 'compact';
The props of Form, each described in its prop list on this page.
interface FormSectionProps {
title?: string;
description?: string;
actions?: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
variant?: FormSectionVariant;
className?: string;
}Buttons#
CancelButton
Props
Events
Called by the component when something happens.
Takes only the props listed.
SaveAndContinueButton
Props
Events
Called by the component when something happens.
Takes only the props listed.
SubmitButton
Props
Takes only the props listed.
Types
FormCancelButton#
FormCancelButton
Props
Events
Called by the component when something happens.
Takes only the props listed.
Types
Other parts#
FormSectionHeader
Props
- Type
string
The section title, an <h3>.
- Type
ReactNode
Optional action(s) on the right of the header row, on the same line as the title: the add button of a list section belongs in the header, not below the grid. Below sm the action drops below the text.
- Type
string
Classes for the header.
- Type
string
A muted line below the title.
- Type
string
Classes for the description.
- Type
string
Classes for the title.
Takes only the props listed.
Types
The props of Form, each described in its prop list on this page.
interface FormSectionHeaderProps {
title: string;
description?: string;
actions?: React.ReactNode;
className?: string;
titleClassName?: string;
descriptionClassName?: string;
}FormActions
Props
- Type
ReactNode
The actions, in reading order. The convention is: cancel on the left, the primary action on the right.
The order is deliberately the caller's responsibility. A cancel/primary prop would introduce roles into this component, and it would then know what "cancel" and "save" mean โ exactly the domain knowledge active- keeps out.
Other props, such as the standard attributes of the underlying element, className and ref, are passed through.
Types
The props of Form, each described in its prop list on this page.
interface FormActionsProps extends React.ComponentProps<'div'> {
children: React.ReactNode;
}