Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3fd0053
chore: Add form page examples
alimpens Aug 8, 2025
23bd6cd
Merge branch 'develop' into chore/DES-1358-add-form-page-examples
alimpens Aug 8, 2025
af04f60
Align landing page with product page
alimpens Aug 8, 2025
97a9619
Merge branch 'chore/DES-1358-add-form-page-examples' of https://githu…
alimpens Aug 8, 2025
c147d49
Add multiple questions page
alimpens Aug 8, 2025
30a9ab8
Make h1 a bit smaller on multiple questions page
alimpens Aug 8, 2025
283cf60
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
alimpens Aug 8, 2025
03fad0a
Add docs, rename page
alimpens Aug 13, 2025
3fbbc1f
Add validation error docs
alimpens Aug 13, 2025
410246b
Add back link to all form pages
alimpens Aug 13, 2025
3864838
Add links to Field and Field Set
alimpens Aug 20, 2025
38df3fa
Rewrite
alimpens Aug 20, 2025
3923b13
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
alimpens Sep 19, 2025
1553655
Merge branch 'develop' into chore/DES-1358-add-form-page-examples
alimpens Sep 19, 2025
512e40a
Fix lint issues
alimpens Sep 19, 2025
3157b76
Merge branch 'develop' into chore/DES-1358-add-form-page-examples
alimpens Sep 24, 2025
2d7a9e3
Add first form page docs
alimpens Sep 24, 2025
0ec29ed
Remove unnecessary brackets
alimpens Sep 26, 2025
c9ffdb7
Use prop for spacing, use correct heading level
alimpens Sep 26, 2025
95bf248
Mostly use spacings from ADS space research
alimpens Sep 26, 2025
c52458c
Use props instead of util classes everywhere
alimpens Sep 26, 2025
ccfa06c
Display levels slightly larger
alimpens Sep 26, 2025
944e8da
Rename props type
alimpens Sep 26, 2025
0e5b292
Use correct heading level
alimpens Sep 26, 2025
abf5550
Merge branch 'develop' into chore/DES-1358-add-form-page-examples
alimpens Sep 26, 2025
3c6b392
Use correct id
alimpens Sep 26, 2025
72e31ee
Add comments
alimpens Sep 26, 2025
360a7df
Add documentation to other pages
alimpens Sep 26, 2025
4cf39ff
Remove braces around arrow function body
VincentSmedinga Oct 1, 2025
2bd7f6e
Add Other category
alimpens Oct 3, 2025
1035211
Make Footer Cell slightly larger
alimpens Oct 3, 2025
d982c26
Show examples of optional and required Radio's and Checkboxes
alimpens Oct 3, 2025
2d8d03f
Merge branch 'main' of https://github.com/Amsterdam/design-system int…
alimpens Oct 3, 2025
777ade4
Merge branch 'develop' of https://github.com/Amsterdam/design-system …
alimpens Oct 3, 2025
9576d66
Use new FieldSet prop instead of custom component
alimpens Oct 3, 2025
133faaf
Add more docs
alimpens Oct 3, 2025
37a5022
Merge branch 'develop' into chore/DES-1358-add-form-page-examples
alimpens Oct 8, 2025
f2744d9
Comment out guideline for now
alimpens Oct 8, 2025
a40c687
Merge branch 'develop' into chore/DES-1358-add-form-page-examples
VincentSmedinga Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions storybook/src/components/Checkbox/Checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const InAFieldSet: Story = {
id="fieldset1"
invalid={invalid}
legend="Waar gaat uw melding over?"
optional
>
<Paragraph className="ams-mb-s" id="description1">
De laatstgenoemde melding.
Expand Down Expand Up @@ -146,16 +147,16 @@ export const InAFieldSetWithValidation: Story = {
</ErrorMessage>
)}
<Column gap="x-small">
<Checkbox invalid={invalid} name="about" value="horeca">
<Checkbox aria-required="true" invalid={invalid} name="about" value="horeca">
Horecabedrijf
</Checkbox>
<Checkbox invalid={invalid} name="about" value="ander_bedrijf">
<Checkbox aria-required="true" invalid={invalid} name="about" value="ander_bedrijf">
Ander soort bedrijf
</Checkbox>
<Checkbox invalid={invalid} name="about" value="evenement">
<Checkbox aria-required="true" invalid={invalid} name="about" value="evenement">
Evenement
</Checkbox>
<Checkbox invalid={invalid} name="about" value="anders">
<Checkbox aria-required="true" invalid={invalid} name="about" value="anders">
Iets anders
</Checkbox>
</Column>
Expand Down
10 changes: 6 additions & 4 deletions storybook/src/components/Radio/Radio.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const InAFieldSet: Story = {
aria-describedby={`description1${invalid ? ' error1' : ''}`}
invalid={invalid}
legend="Waar gaat uw melding over?"
optional
role="radiogroup"
>
<Paragraph className="ams-mb-s" id="description1">
Expand Down Expand Up @@ -125,6 +126,7 @@ export const InAFieldSetWithValidation: Story = {
render: ({ invalid }) => (
<FieldSet
aria-describedby={`description2${invalid ? ' error2' : ''}`}
aria-required="true"
invalid={invalid}
legend="Waar gaat uw melding over?"
role="radiogroup"
Expand All @@ -138,16 +140,16 @@ export const InAFieldSetWithValidation: Story = {
</ErrorMessage>
)}
<Column gap="x-small">
<Radio invalid={invalid} name="about" value="horeca">
<Radio aria-required="true" invalid={invalid} name="about" value="horeca">
Horecabedrijf
</Radio>
<Radio invalid={invalid} name="about" value="ander_bedrijf">
<Radio aria-required="true" invalid={invalid} name="about" value="ander_bedrijf">
Ander soort bedrijf
</Radio>
<Radio invalid={invalid} name="about" value="evenement">
<Radio aria-required="true" invalid={invalid} name="about" value="evenement">
Evenement
</Radio>
<Radio invalid={invalid} name="about" value="anders">
<Radio aria-required="true" invalid={invalid} name="about" value="anders">
Iets anders
</Radio>
</Column>
Expand Down
69 changes: 69 additions & 0 deletions storybook/src/pages/amsterdam/FormFlow/FormFlow.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{/* @license CC0-1.0 */}

import { Meta } from "@storybook/addon-docs/blocks";
import * as FormStories from "./FormFlow.stories.tsx";

<Meta of={FormStories} />

# Form Flow

These examples describe a user-friendly form flow.
Form pages have a minimal header and footer in order to remove distractions.
Start with asking [one thing per page](https://www.gov.uk/service-manual/design/form-structure#start-with-one-thing-per-page).
This can be a single question, or a group of related questions.

## Landing Page

Most form flows start with a landing page that explains the process and provides a call to action to start the form flow.

{/* prettier-ignore */}
{/*
TODO: Decide what we want to do here first.

## First Form Page

The first form page is the same as other form pages, with one exception: it does not have a back link.
A back link helps users feel secure that their progress is preserved while moving between form pages.
Since the first form page has no progress to save yet, a back link is unnecessary there.
Additionally, users might reach the first form page from a source other than the landing page, which could make a back link confusing.

\*/}

## With One Question

On a form page with one question, the label or legend for the question is the level 1 heading of the page.
For labels, wrap the `h1` around it.
For legends, wrap it around the `h1`.

For more information, see [Making labels and legends headings](https://design-system.service.gov.uk/get-started/labels-legends-headings/).

## With Multiple Questions

When a form page has multiple questions, place an `h1` outside the `form`-tag,
which describes the group of questions.

For more information, see [Question pages - Asking complex questions without using hint text](https://design-system.service.gov.uk/patterns/question-pages/#asking-complex-questions-without-using-hint-text)

## With Validation Error

If the user’s answers fail validation:

- show them the page again, with the form fields as the user filled them in
- show an [Invalid Form Alert](/docs/components-forms-invalid-form-alert--docs) at the top of the `main` container, and move keyboard focus to it
- add ‘(X invoerfouten) ’ to the beginning of the page `<title>` so that screen readers announce it immediately (Invalid Form Alert does this automatically)
- show [Error message](/docs/components-forms-error-message--docs) components next to fields with errors

See the [Field](/docs/components-forms-field--docs#with-validation) and [Field Set](/docs/components-forms-field-set--docs#with-validation) docs on how to mark them as invalid.

Validate the user’s answers when they click on the submit button.
Generally speaking, avoid validating the information in a field before the user has finished entering it or when they move away from the field.

For more information:

- [Recover from validation errors](https://design-system.service.gov.uk/patterns/validation/)
- [Error summary](https://design-system.service.gov.uk/components/error-summary/)

## References

- [Gov.uk on question pages](https://design-system.service.gov.uk/patterns/question-pages/)
- [NL Design System on forms](https://nldesignsystem.nl/richtlijnen/formulieren/)
36 changes: 36 additions & 0 deletions storybook/src/pages/amsterdam/FormFlow/FormFlow.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { Meta, StoryObj } from '@storybook/react-vite'

import { LandingPage as LandingPageComponent } from './LandingPage'
import { WithMultipleQuestions as WithMultipleQuestionsComponent } from './WithMultipleQuestions'
import { WithOneQuestion as WithOneQuesionComponent } from './WithOneQuestion'
import { WithValidationError as WithValidationErrorComponent } from './WithValidationError'

const meta = {
title: 'Pages/Amsterdam.nl/Form Flow',
component: LandingPageComponent,
parameters: {
layout: 'fullscreen',
themes: { themeOverride: 'Spacious' },
},
} satisfies Meta<typeof LandingPageComponent>

export default meta

export const LandingPage: StoryObj = {}

export const WithOneQuestion: StoryObj = {
render: () => <WithOneQuesionComponent />,
}

export const WithMultipleQuestions: StoryObj = {
render: () => <WithMultipleQuestionsComponent />,
}

export const WithValidationError: StoryObj = {
render: () => <WithValidationErrorComponent />,
}
40 changes: 40 additions & 0 deletions storybook/src/pages/amsterdam/FormFlow/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { CallToActionLink, Grid, Heading, OrderedList, Paragraph } from '@amsterdam/design-system-react'

import { Layout } from '../common/Layout'

export const LandingPage = () => (
<Layout>
<Grid as="main" id="inhoud" paddingBottom="2x-large" paddingTop="large">
<Grid.Cell span={{ narrow: 4, medium: 5, wide: 7 }} start={{ narrow: 1, medium: 2, wide: 3 }}>
<Heading className="ams-mb-m" level={1}>
Waar u dit formulier voor gebruikt
</Heading>
<Paragraph className="ams-mb-xl" size="large">
Met dit formulier maakt u een afspraak bij een Stadsloket in Amsterdam of Weesp.
</Paragraph>

<Heading className="ams-mb-s" level={2}>
De stappen in dit formulier
</Heading>
<OrderedList className="ams-mb-l">
<OrderedList.Item>
<strong>Afspraak</strong> - Kies waarvoor u een afspraak wilt maken. Kies ook waar u de afspraak wilt
hebben. En wanneer.
</OrderedList.Item>
<OrderedList.Item>
<strong>Uw gegevens</strong> - Vul uw contactgegevens in.
</OrderedList.Item>
<OrderedList.Item>
<strong>Controleren</strong> - Controleer de gegevens die u heeft ingevuld. Verstuur de aanvraag.
</OrderedList.Item>
</OrderedList>
<CallToActionLink href="#">Start het formulier</CallToActionLink>
</Grid.Cell>
</Grid>
</Layout>
)
106 changes: 106 additions & 0 deletions storybook/src/pages/amsterdam/FormFlow/WithMultipleQuestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import {
Button,
Field,
Grid,
Heading,
Label,
Page,
PageHeader,
Paragraph,
StandaloneLink,
TextInput,
} from '@amsterdam/design-system-react'
import { ChevronBackwardIcon } from '@amsterdam/design-system-react-icons'

import { FormFooter } from './components/FormFooter'

export const WithMultipleQuestions = () => (
<Page>
{/* Keep the Page Header as simple as possible, to avoid distractions and to prevent users from accidentally navigating away from the form flow. */}
<PageHeader className="ams-mb-xl" />
{/* The back link is in its own Grid, because it should be outside of the main section. */}
<Grid className="ams-mb-s">
<Grid.Cell span={{ narrow: 4, medium: 6, wide: 6 }} start={{ narrow: 1, medium: 2, wide: 3 }}>
{/*
* We add a back link to allow users to navigate between form pages, without having to worry about losing their progress.
* Using the browser back button should also work without losing progress, but users do not always trust it (for some applications, rightfully so).
* For more info, see: https://design-system.service.gov.uk/components/back-link/
*
* We use a link here instead of a button, because multiple submit buttons in a form can cause unexpected behavior.
* For more info, see: https://adamsilver.io/blog/forms-with-multiple-submit-buttons-are-problematic/
*/}
<StandaloneLink href="#" icon={ChevronBackwardIcon}>
Vorige vraag
</StandaloneLink>
</Grid.Cell>
</Grid>
<Grid as="main" paddingBottom="2x-large">
<Grid.Cell span={{ narrow: 4, medium: 6, wide: 6 }} start={{ narrow: 1, medium: 2, wide: 3 }}>
{/*
* We use a header here that is labelled by an aria-hidden heading, so that we communicate a labelled
* section to screen readers, without adding an unnecessary heading to the heading hierarchy.
* Furthermore, if CSS fails to load, we still have a clearly labelled section.
*/}
<header aria-labelledby="form-header" className="ams-mb-m ams-gap-xs">
<Heading aria-hidden id="form-header" level={2} size="level-4">
Afspraak maken
</Heading>
{/*
* Start by testing your form without a progress indicator to see if it’s simple enough that users do not need one.
* If you do, use a simple one like this one.
* For more info, see: https://design-system.service.gov.uk/patterns/question-pages/#using-progress-indicators
*/}
<Paragraph>Stap 2 van 3: Uw gegevens</Paragraph>
</header>
<Heading className="ams-mb-m" level={1} size="level-3">
Contactgegevens
</Heading>
{/*
* Do not use HTML5 form validation, because it is not consistent across browsers and devices,
* and often gives the user too little information.
* Preferably validate user input on the server and return it to the client.
* If that is not possible, use client-side validation.
* For more info, see: https://nldesignsystem.nl/richtlijnen/formulieren/foutmeldingen/html-formuliervalidatie/#gebruik-geen-html-formuliervalidatie
*/}
<form noValidate onSubmit={(e) => e.preventDefault()}>
{/* See the docs on specific form components on https://designsystem.amsterdam for more information on how to use them */}
<Field>
<Label htmlFor="email-input" optional>
E-mailadres
</Label>
<TextInput
autoComplete="email"
autoCorrect="off"
className="ams-mb-m"
id="email-input"
name="email"
size={30} // Based on this recommendation: https://design-system.service.gov.uk/patterns/email-addresses/#help-users-to-enter-a-valid-email-address
spellCheck="false"
type="email"
/>
</Field>
<Field className="ams-mb-l">
<Label htmlFor="tel-input" optional>
Telefoonnummer
</Label>
<TextInput
autoComplete="tel"
className="ams-mb-m"
id="tel-input"
name="phone"
size={15} // Phone numbers have a maximum length of 15 characters, as per E.164 standard: https://en.wikipedia.org/wiki/E.164
type="tel"
/>
</Field>
<Button type="submit">Volgende vraag</Button>
</form>
</Grid.Cell>
</Grid>
<FormFooter />
</Page>
)
Loading
Loading