@canonical/react-ds-global-form
v0.30.0
Published
Form components for the Pragma design system. This package provides a field system built on react-hook-form with two core patterns: a field switch for rendering different input types and middleware composition for extending field behavior.
Maintainers
Keywords
Readme
@canonical/react-ds-global-form
Form components for the Pragma design system. This package provides a field system built on react-hook-form with two core patterns: a field switch for rendering different input types and middleware composition for extending field behavior.
Prerequisites
- React 19 or higher
Installation
bun add @canonical/react-ds-global-form @canonical/stylesImport the global styles and the form component styles in your application's root stylesheet:
@import url("@canonical/styles");
@import url("@canonical/react-ds-global-form/dist/esm/index.css");The global styles provide the CSS reset, typography baseline, and design tokens (colour, spacing, surfaces, states) that all form components depend on. The form stylesheet provides input chrome, field layout, and component-specific styles.
The package builds on top of @canonical/react-ds-global.
Dependencies
The form system builds on two key libraries:
- react-hook-form - Form state management, validation, and field registration. The
Fieldcomponent usesuseFormContextinternally, so forms must be wrapped in aFormProvider. - downshift - Powers the combobox field with accessible autocomplete behavior.
Usage
Wrap your form in a Form component and use Field for inputs:
import { Form, Field } from "@canonical/react-ds-global-form";
function ContactForm() {
const onSubmit = (data) => console.log(data);
return (
<Form onSubmit={onSubmit}>
<Field
name="email"
inputType="email"
label="Email address"
description="We'll never share your email."
/>
<Field
name="message"
inputType="textarea"
label="Message"
/>
<button type="submit">Send</button>
</Form>
);
}Form modes
Form wraps its children in a react-hook-form FormProvider and works in two modes:
Internal mode (above) — pass
onSubmit, and optionallydefaultValuesand a validationmode;Formcreates theuseForminstance for you. Best for a self-contained form.External mode — create the
useForminstance yourself and pass it asmethods. Because you own it, you can readformState(e.g.isSubmitting) and call its methods (reset,setValue,watch, …). Use this for async submits, shared state, or multi-step forms.import { useForm } from "react-hook-form"; const methods = useForm({ mode: "onBlur", defaultValues: { email: "" } }); const { reset, formState: { isSubmitting } } = methods; <Form methods={methods} onSubmit={async (data) => { await save(data); reset(); }}> <Field name="email" inputType="email" label="Email" /> <button type="submit" disabled={isSubmitting}>Send</button> </Form>;When you pass
methods,Form's owndefaultValues/modeprops are ignored — configure those on youruseFormcall.
This library is a thin layer over react-hook-form
(^7.71): validation rules (registerProps), formState, submission, field
arrays, and schema resolvers are all RHF's API. See the Getting Started guide
in Storybook for a full walkthrough with runnable examples, and the
react-hook-form docs for the complete surface.
Field Switch Pattern
The Field component uses inputType to select the appropriate input component:
| inputType | Component | Description |
|-----------|-----------|-------------|
| text, email, password, number, tel, url | Text | Standard text inputs |
| textarea | Textarea | Multi-line text |
| checkbox | Checkbox | Boolean toggle |
| range | Range | Slider input |
| select | Select | Dropdown selection |
| simple-choices | SimpleChoices | Radio buttons or checkboxes |
| combobox | Combobox | Searchable dropdown |
| hidden | Hidden | Hidden input |
| custom | Your component | Pass via CustomComponent prop |
All fields are wrapped with withWrapper, which provides form registration, labels, descriptions, error display, and middleware support.
Custom Fields
For field types not covered by the built-ins, use inputType="custom":
import { Field } from "@canonical/react-ds-global-form";
import { MyColorPicker } from "./MyColorPicker";
<Field
name="brandColor"
inputType="custom"
CustomComponent={MyColorPicker}
label="Brand Color"
/>Custom components must be wrapped with withWrapper. Custom components must satisfy the InputProps type and integrate with react-hook-form via useFormContext.
Middleware Pattern
Middleware are higher-order components that wrap fields to add functionality. They compose via the middleware prop:
<Field
name="country"
inputType="select"
label="Country"
middleware={[addRESTOptions("/api/countries")]}
/>The middleware signature is (Component) => Component. Multiple middleware compose in array order, with the first middleware as the outermost wrapper.
Built-in Middleware
addRESTOptions - Fetches options from an API endpoint:
import { addRESTOptions } from "@canonical/react-ds-global-form";
<Field
name="category"
inputType="select"
middleware={[
addRESTOptions("/api/categories", {
transformData: (data) => data.categories,
}),
]}
/>addRESTValidation - Validates field values against an API:
import { addRESTValidation } from "@canonical/react-ds-global-form";
<Field
name="username"
inputType="text"
middleware={[
addRESTValidation("/api/validate-username", {
debounceWait: 300,
minLength: 3,
}),
]}
/>Custom middleware follows the same factory pattern: an outer function accepts configuration and returns a HOC.
Conditional Display
Fields can conditionally render based on other field values:
<Field
name="company"
inputType="text"
label="Company"
condition={[
["accountType"],
([type]) => type === "business",
]}
/>The field only renders when the condition function returns true.
Storybook
cd packages/react/ds-global-form
bun run storybookThe Storybook configuration includes MSW integration for mocking backend responses.
Component Specifications
Form component specifications are defined in the Design System Ontology.
