diff --git a/packages/css/src/components/error-message/README.md b/packages/css/src/components/error-message/README.md
new file mode 100644
index 0000000000..779ef3cd4e
--- /dev/null
+++ b/packages/css/src/components/error-message/README.md
@@ -0,0 +1,11 @@
+
+
+# Error Message
+
+Show an error message when there is a form field validation error.
+In the error message explain what went wrong and how to fix it.
+
+For guidance and examples on using error messages in a form,
+refer to the [Field](/docs/components-forms-field--docs) and [Field Set](/docs/components-forms-field-set--docs) documentation.
+
+Read the documentation by [NL Design System](https://www.nldesignsystem.nl/richtlijnen/formulieren/foutmeldingen) and [Gov.uk](https://design-system.service.gov.uk/components/error-message/) for more information on the contents of error messages and when to show them.
diff --git a/packages/css/src/components/error-message/error-message.scss b/packages/css/src/components/error-message/error-message.scss
new file mode 100644
index 0000000000..4dd6e3fd19
--- /dev/null
+++ b/packages/css/src/components/error-message/error-message.scss
@@ -0,0 +1,22 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright Gemeente Amsterdam
+ */
+
+@import "../../common/text-rendering";
+
+@mixin reset {
+ box-sizing: border-box;
+ margin-block: 0;
+}
+
+.ams-error-message {
+ color: var(--ams-error-message-color);
+ font-family: var(--ams-error-message-font-family);
+ font-size: var(--ams-error-message-font-size);
+ font-weight: var(--ams-error-message-font-weight);
+ line-height: var(--ams-error-message-line-height);
+
+ @include text-rendering;
+ @include reset;
+}
diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss
index 5f7cf2d09a..92badfec90 100644
--- a/packages/css/src/components/index.scss
+++ b/packages/css/src/components/index.scss
@@ -4,6 +4,7 @@
*/
/* Append here */
+@import "./error-message/error-message";
@import "./file-input/file-input";
@import "./field/field";
@import "./select/select";
diff --git a/packages/react/src/ErrorMessage/ErrorMessage.test.tsx b/packages/react/src/ErrorMessage/ErrorMessage.test.tsx
new file mode 100644
index 0000000000..4026873e78
--- /dev/null
+++ b/packages/react/src/ErrorMessage/ErrorMessage.test.tsx
@@ -0,0 +1,51 @@
+import { render, screen } from '@testing-library/react'
+import { createRef } from 'react'
+import { ErrorMessage } from './ErrorMessage'
+import '@testing-library/jest-dom'
+
+describe('Error message', () => {
+ it('renders', () => {
+ render()
+ const component = screen.getByRole('paragraph')
+
+ expect(component).toBeInTheDocument()
+ expect(component).toBeVisible()
+ })
+
+ it('renders a design system BEM class name', () => {
+ render()
+ const component = screen.getByRole('paragraph')
+
+ expect(component).toHaveClass('ams-error-message')
+ })
+
+ it('renders an additional class name', () => {
+ render()
+ const component = screen.getByRole('paragraph')
+
+ expect(component).toHaveClass('ams-error-message extra')
+ })
+
+ it('renders a Dutch prefix by default', () => {
+ render()
+ const component = screen.getByText('Invoerfout', { exact: false })
+
+ expect(component).toBeInTheDocument()
+ })
+
+ it('renders a custom prefix', () => {
+ render()
+ const component = screen.getByText('Error', { exact: false })
+
+ expect(component).toBeInTheDocument()
+ })
+
+ it('supports ForwardRef in React', () => {
+ const ref = createRef()
+
+ render()
+ const component = screen.getByRole('paragraph')
+
+ expect(ref.current).toBe(component)
+ })
+})
diff --git a/packages/react/src/ErrorMessage/ErrorMessage.tsx b/packages/react/src/ErrorMessage/ErrorMessage.tsx
new file mode 100644
index 0000000000..ce9b2e1c0a
--- /dev/null
+++ b/packages/react/src/ErrorMessage/ErrorMessage.tsx
@@ -0,0 +1,31 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright Gemeente Amsterdam
+ */
+
+import clsx from 'clsx'
+import { forwardRef } from 'react'
+import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'
+import { VisuallyHidden } from '../VisuallyHidden'
+
+export type ErrorMessageProps = {
+ /** An accessible phrase that screen readers announce before the error message. Should translate to something like ‘input error’. */
+ prefix?: string
+} & PropsWithChildren>
+
+export const ErrorMessage = forwardRef(
+ (
+ { children, className, prefix = 'Invoerfout', ...restProps }: ErrorMessageProps,
+ ref: ForwardedRef,
+ ) => (
+
+
+ {prefix}
+ {': '}
+
+ {children}
+
+ ),
+)
+
+ErrorMessage.displayName = 'ErrorMessage'
diff --git a/packages/react/src/ErrorMessage/README.md b/packages/react/src/ErrorMessage/README.md
new file mode 100644
index 0000000000..b7a1e98d1a
--- /dev/null
+++ b/packages/react/src/ErrorMessage/README.md
@@ -0,0 +1,5 @@
+
+
+# React Error Message component
+
+[Error Message documentation](../../../css/src/components/error-message/README.md)
diff --git a/packages/react/src/ErrorMessage/index.ts b/packages/react/src/ErrorMessage/index.ts
new file mode 100644
index 0000000000..50e3b5c5c3
--- /dev/null
+++ b/packages/react/src/ErrorMessage/index.ts
@@ -0,0 +1,2 @@
+export { ErrorMessage } from './ErrorMessage'
+export type { ErrorMessageProps } from './ErrorMessage'
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 4334528431..c7bd65c925 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -4,6 +4,7 @@
*/
/* Append here */
+export * from './ErrorMessage'
export * from './FileInput'
export * from './Field'
export * from './Select'
diff --git a/proprietary/tokens/src/components/ams/error-message.tokens.json b/proprietary/tokens/src/components/ams/error-message.tokens.json
new file mode 100644
index 0000000000..a742de7279
--- /dev/null
+++ b/proprietary/tokens/src/components/ams/error-message.tokens.json
@@ -0,0 +1,11 @@
+{
+ "ams": {
+ "error-message": {
+ "color": { "value": "{ams.color.primary-red}" },
+ "font-family": { "value": "{ams.text.font-family}" },
+ "font-size": { "value": "{ams.text.level.6.font-size}" },
+ "font-weight": { "value": "{ams.text.font-weight.normal}" },
+ "line-height": { "value": "{ams.text.level.6.line-height}" }
+ }
+ }
+}
diff --git a/storybook/src/components/ErrorMessage/ErrorMessage.docs.mdx b/storybook/src/components/ErrorMessage/ErrorMessage.docs.mdx
new file mode 100644
index 0000000000..74fae9a038
--- /dev/null
+++ b/storybook/src/components/ErrorMessage/ErrorMessage.docs.mdx
@@ -0,0 +1,19 @@
+import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks";
+import * as ErrorMessageStories from "./ErrorMessage.stories.tsx";
+import README from "../../../../packages/css/src/components/error-message/README.md?raw";
+
+
+
+{README}
+
+
+
+
+
+## With a custom prefix
+
+Error messages are automatically prefixed with a visually hidden text, the Dutch word "Invoerfout".
+This makes the error message more clear for screen reader users.
+If you want to change this prefix, to support another language for example, you can use the `prefix` prop.
+
+
diff --git a/storybook/src/components/ErrorMessage/ErrorMessage.stories.tsx b/storybook/src/components/ErrorMessage/ErrorMessage.stories.tsx
new file mode 100644
index 0000000000..8e6c3e4312
--- /dev/null
+++ b/storybook/src/components/ErrorMessage/ErrorMessage.stories.tsx
@@ -0,0 +1,33 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright Gemeente Amsterdam
+ */
+
+import { ErrorMessage } from '@amsterdam/design-system-react/src'
+import { Meta, StoryObj } from '@storybook/react'
+
+const meta = {
+ title: 'Components/Forms/Error Message',
+ component: ErrorMessage,
+ args: {
+ children: 'Vul een geldig e-mailadres in, bijvoorbeeld naam@voorbeeld.nl.',
+ },
+ argTypes: {
+ children: {
+ table: { disable: false },
+ },
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
+
+export const WithCustomPrefix: Story = {
+ args: {
+ children: 'Enter an email address in the correct format, like name@example.com',
+ prefix: 'Error',
+ },
+}
diff --git a/storybook/src/components/Field/Field.docs.mdx b/storybook/src/components/Field/Field.docs.mdx
index c2283e9caf..4a0eb86187 100644
--- a/storybook/src/components/Field/Field.docs.mdx
+++ b/storybook/src/components/Field/Field.docs.mdx
@@ -14,7 +14,7 @@ import README from "../../../../packages/css/src/components/field/README.md?raw"
A Field can have a description.
Make sure to connect this description to the input in the Field,
-otherwise this won’t be read by a screen reader.
+otherwise it won’t be read by a screen reader.
Add an `aria-describedby` attribute to the input and provide the `id` of the describing element as its value.
@@ -22,5 +22,9 @@ Add an `aria-describedby` attribute to the input and provide the `id` of the des
## With Error
A Field can indicate if the contained input has a validation error.
+Use Error Message to describe the error.
+Make sure to connect the error message to the input in the Field,
+otherwise it won’t be read by a screen reader.
+Add an `aria-describedby` attribute to the input and provide the `id` of Error Message as its value.
diff --git a/storybook/src/components/Field/Field.stories.tsx b/storybook/src/components/Field/Field.stories.tsx
index c1559c47a3..e542c88ea8 100644
--- a/storybook/src/components/Field/Field.stories.tsx
+++ b/storybook/src/components/Field/Field.stories.tsx
@@ -3,8 +3,8 @@
* Copyright Gemeente Amsterdam
*/
-import { TextInput } from '@amsterdam/design-system-react'
-import { Field, Label, Paragraph } from '@amsterdam/design-system-react/src'
+import { ErrorMessage, Label, TextInput } from '@amsterdam/design-system-react'
+import { Field, Paragraph } from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
@@ -48,7 +48,8 @@ export const WithError: Story = {
Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u.
-
+ Geef aan waar het om gaat.
+
),
}
diff --git a/storybook/src/components/FieldSet/FieldSet.docs.mdx b/storybook/src/components/FieldSet/FieldSet.docs.mdx
index 4214143dec..506cab411d 100644
--- a/storybook/src/components/FieldSet/FieldSet.docs.mdx
+++ b/storybook/src/components/FieldSet/FieldSet.docs.mdx
@@ -25,6 +25,10 @@ and provide the `id` of the describing element as its value.
## With Error
A Field Set can indicate whether any of the inputs it contains has a validation error.
+Use Error Message to describe the error.
+Make sure to connect the error message to the correct input in the Field Set,
+otherwise it won’t be read by a screen reader.
+Add an `aria-describedby` attribute to the input and provide the `id` of Error Message as its value.
@@ -32,12 +36,19 @@ A Field Set can indicate whether any of the inputs it contains has a validation
Use a Field Set to group radio buttons.
When grouping radio inputs, use `role="radiogroup"` on Field Set to have this grouping explicitly announced as a radio group (the default role is `group`).
-
Using `role="radiogroup"` also allows you to use `aria-required` on Field Set, which isn’t allowed for role `group`.
Always also set `aria-required` on the individual radio buttons though, to make sure it’s read by screen readers.
+### Radio group with error
+
+A Field Set with a radio button group can also have a validation error.
+In this case, connect the error message to the Field Set instead of an input.
+Add an `aria-describedby` attribute to the Field Set and provide the `id` of Error Message as its value.
+
+
+
### Checkbox group
Use a Field Set to group checkboxes.
@@ -48,3 +59,9 @@ not report a description connected to a Field Set when it contains checkboxes.
Try to avoid using descriptions for Field Sets containing checkboxes for this reason.
+
+### Checkbox group with error
+
+Because of [the NVDA bug mentioned earlier](https://github.com/nvaccess/nvda/issues/12718),
+we currently do not have a reliable way to report error messages for checkbox groups with a validation error.
+We are working on adding this as soon as possible.
diff --git a/storybook/src/components/FieldSet/FieldSet.stories.tsx b/storybook/src/components/FieldSet/FieldSet.stories.tsx
index 501d5c56bd..1aed9b47b9 100644
--- a/storybook/src/components/FieldSet/FieldSet.stories.tsx
+++ b/storybook/src/components/FieldSet/FieldSet.stories.tsx
@@ -3,7 +3,16 @@
* Copyright Gemeente Amsterdam
*/
-import { Checkbox, Column, FieldSet, Label, Paragraph, Radio, TextInput } from '@amsterdam/design-system-react/src'
+import {
+ Checkbox,
+ Column,
+ ErrorMessage,
+ FieldSet,
+ Label,
+ Paragraph,
+ Radio,
+ TextInput,
+} from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'
const meta = {
@@ -64,9 +73,11 @@ export const WithError: Story = {
-
+ {args.invalid && Vul uw voornaam in.}
+
-
+ {args.invalid && Vul uw achternaam in.}
+
),
@@ -105,6 +116,45 @@ export const RadioGroup: Story = {
),
}
+export const RadioGroupWithError: Story = {
+ args: {
+ legend: 'Waar gaat uw melding over?',
+ invalid: true,
+ },
+ render: (args) => (
+
+ ),
+}
+
export const CheckboxGroup: Story = {
args: {
legend: 'Waar gaat uw melding over?',
@@ -128,3 +178,29 @@ export const CheckboxGroup: Story = {
),
}
+
+// export const CheckboxGroupWithError: Story = {
+// args: {
+// invalid: true,
+// legend: 'Waar gaat uw melding over?',
+// },
+// render: (args) => (
+//
+// ),
+// }