diff --git a/documentation/storybook.md b/documentation/storybook.md
index 37bc245ccf..c98f275901 100644
--- a/documentation/storybook.md
+++ b/documentation/storybook.md
@@ -22,9 +22,11 @@ We write our documentation in Dutch.
## Best practices for controls
-1. Use radio buttons rather than a dropdown for props with 5 options or less.
+1. For props offering five options or less, use radio buttons rather than a select.
This makes it easier to compare the options.
It saves the user a click to select each option and shows everything up front.
+2. Don’t use inline radios.
+ Their options appear rather small, making them difficult to target with a pointing device.
More to follow.
diff --git a/packages/css/src/components/badge/README.md b/packages/css/src/components/badge/README.md
new file mode 100644
index 0000000000..740d4831b0
--- /dev/null
+++ b/packages/css/src/components/badge/README.md
@@ -0,0 +1,10 @@
+# Badge
+
+A prominently coloured box containing 1 or 2 words.
+Guides the user in taking a specific action or describes its surrounding content.
+
+## Design
+
+The badge can contain a short text or a number.
+The default background colour is dark green.
+Suggestions on when to use the other colours will follow soon.
diff --git a/packages/css/src/components/badge/badge.scss b/packages/css/src/components/badge/badge.scss
new file mode 100644
index 0000000000..1c20a45492
--- /dev/null
+++ b/packages/css/src/components/badge/badge.scss
@@ -0,0 +1,58 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright (c) 2024 Gemeente Amsterdam
+ */
+
+.amsterdam-badge {
+ display: inline-block;
+ font-family: var(--amsterdam-badge-font-family);
+ font-size: var(--amsterdam-badge-spacious-font-size);
+ font-weight: var(--amsterdam-badge-font-weight);
+ line-height: var(--amsterdam-badge-spacious-line-height);
+ padding-inline: var(--amsterdam-badge-padding-inline);
+
+ .amsterdam-theme--compact & {
+ font-size: var(--amsterdam-badge-compact-font-size);
+ line-height: var(--amsterdam-badge-compact-line-height);
+ }
+}
+
+.amsterdam-badge--blue {
+ background-color: var(--amsterdam-badge-blue-background-color);
+ color: var(--amsterdam-badge-blue-color);
+}
+
+.amsterdam-badge--dark-blue {
+ background-color: var(--amsterdam-badge-dark-blue-background-color);
+ color: var(--amsterdam-badge-dark-blue-color);
+}
+
+.amsterdam-badge--dark-green {
+ background-color: var(--amsterdam-badge-dark-green-background-color);
+ color: var(--amsterdam-badge-dark-green-color);
+}
+
+.amsterdam-badge--green {
+ background-color: var(--amsterdam-badge-green-background-color);
+ color: var(--amsterdam-badge-green-color);
+}
+
+.amsterdam-badge--magenta {
+ background-color: var(--amsterdam-badge-magenta-background-color);
+ color: var(--amsterdam-badge-magenta-color);
+}
+
+.amsterdam-badge--orange {
+ background-color: var(--amsterdam-badge-orange-background-color);
+ color: var(--amsterdam-badge-orange-color);
+}
+
+.amsterdam-badge--purple {
+ background-color: var(--amsterdam-badge-purple-background-color);
+ color: var(--amsterdam-badge-purple-color);
+}
+
+.amsterdam-badge--yellow {
+ background-color: var(--amsterdam-badge-yellow-background-color);
+ color: var(--amsterdam-badge-yellow-color);
+}
diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss
index f4374cd68d..247c622e19 100644
--- a/packages/css/src/components/index.scss
+++ b/packages/css/src/components/index.scss
@@ -4,6 +4,7 @@
*/
/* Append here */
+@import "./badge/badge";
@import "./table/table";
@import "./mega-menu/mega-menu";
@import "./icon-button/icon-button";
diff --git a/packages/react/package.json b/packages/react/package.json
index 072de123b3..274851c20d 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -63,6 +63,7 @@
"react-dom": "18.2.0",
"rollup": "4.9.4",
"rollup-plugin-delete": "2.0.0",
+ "rollup-plugin-dts": "6.1.0",
"rollup-plugin-filesize": "10.0.0",
"rollup-plugin-node-externals": "6.1.2",
"rollup-plugin-node-polyfills": "0.2.1",
diff --git a/packages/react/rollup.config.mjs b/packages/react/rollup.config.mjs
index b8dd0d2570..fa3a4fdee0 100644
--- a/packages/react/rollup.config.mjs
+++ b/packages/react/rollup.config.mjs
@@ -7,6 +7,8 @@ import nodeExternal from 'rollup-plugin-node-externals'
import nodePolyfills from 'rollup-plugin-node-polyfills'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import typescript from 'rollup-plugin-typescript2'
+import dts from 'rollup-plugin-dts'
+import del from 'rollup-plugin-delete'
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'))
@@ -46,7 +48,7 @@ export default [
include: /node_modules/,
}),
nodePolyfills(),
- typescript({ includeDependencies: false }),
+ typescript({ includeDependencies: false, useTsconfigDeclarationDir: true }),
babel({
presets: ['@babel/preset-react'],
babelHelpers: 'runtime',
@@ -58,4 +60,15 @@ export default [
filesize(),
],
},
+ {
+ input: './dist/dts/index.d.ts',
+ output: [{ file: 'dist/index.d.ts', format: 'es' }],
+ plugins: [
+ dts(),
+ del({
+ targets: 'dist/dts',
+ hook: 'buildEnd',
+ }),
+ ],
+ },
]
diff --git a/packages/react/src/Badge/Badge.test.tsx b/packages/react/src/Badge/Badge.test.tsx
new file mode 100644
index 0000000000..5d029e2d57
--- /dev/null
+++ b/packages/react/src/Badge/Badge.test.tsx
@@ -0,0 +1,67 @@
+import { render } from '@testing-library/react'
+import { createRef } from 'react'
+import { Badge, badgeColors } from './Badge'
+import '@testing-library/jest-dom'
+
+describe('Badge', () => {
+ it('renders', () => {
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(component).toBeInTheDocument()
+ expect(component).toBeVisible()
+ })
+
+ it('renders a design system BEM class name', () => {
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(component).toHaveClass('amsterdam-badge')
+ })
+
+ it('renders an additional class name', () => {
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(component).toHaveClass('amsterdam-badge extra')
+ })
+
+ it('supports ForwardRef in React', () => {
+ const ref = createRef()
+
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(ref.current).toBe(component)
+ })
+
+ it('renders with a number label', () => {
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(component).toHaveTextContent('1')
+ })
+
+ it('renders with default color', () => {
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(component).toHaveClass('amsterdam-badge--dark-green')
+ })
+
+ badgeColors.map((color) =>
+ it(`renders with ${color} color`, () => {
+ const { container } = render()
+
+ const component = container.querySelector(':only-child')
+
+ expect(component).toHaveClass(`amsterdam-badge--${color}`)
+ }),
+ )
+})
diff --git a/packages/react/src/Badge/Badge.tsx b/packages/react/src/Badge/Badge.tsx
new file mode 100644
index 0000000000..56a1571aaa
--- /dev/null
+++ b/packages/react/src/Badge/Badge.tsx
@@ -0,0 +1,36 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright (c) 2024 Gemeente Amsterdam
+ */
+
+import clsx from 'clsx'
+import { forwardRef } from 'react'
+import type { ForwardedRef, HTMLAttributes } from 'react'
+
+export const badgeColors = [
+ 'blue',
+ 'dark-blue',
+ 'dark-green',
+ 'green',
+ 'magenta',
+ 'orange',
+ 'purple',
+ 'yellow',
+] as const
+
+type BadgeColor = (typeof badgeColors)[number]
+
+export type BadgeProps = {
+ color?: BadgeColor
+ label: string | number
+} & HTMLAttributes
+
+export const Badge = forwardRef(
+ ({ label, className, color = 'dark-green', ...restProps }: BadgeProps, ref: ForwardedRef) => (
+
+ {label}
+
+ ),
+)
+
+Badge.displayName = 'Badge'
diff --git a/packages/react/src/Badge/README.md b/packages/react/src/Badge/README.md
new file mode 100644
index 0000000000..b34e77c7c1
--- /dev/null
+++ b/packages/react/src/Badge/README.md
@@ -0,0 +1,3 @@
+# React Badge component
+
+[Badge documentation](../../../css/src/badge/README.md)
diff --git a/packages/react/src/Badge/index.ts b/packages/react/src/Badge/index.ts
new file mode 100644
index 0000000000..292a59dbf2
--- /dev/null
+++ b/packages/react/src/Badge/index.ts
@@ -0,0 +1,2 @@
+export { Badge } from './Badge'
+export type { BadgeProps } from './Badge'
diff --git a/packages/react/src/Breadcrumb/Breadcrumb.test.tsx b/packages/react/src/Breadcrumb/Breadcrumb.test.tsx
index d5420454df..591dc45b36 100644
--- a/packages/react/src/Breadcrumb/Breadcrumb.test.tsx
+++ b/packages/react/src/Breadcrumb/Breadcrumb.test.tsx
@@ -4,6 +4,25 @@ import { Breadcrumb } from './Breadcrumb'
import '@testing-library/jest-dom'
describe('Breadcrumb', () => {
+ it('renders', () => {
+ render()
+ const component = screen.getByRole('navigation')
+ expect(component).toBeInTheDocument()
+ expect(component).toBeVisible()
+ })
+
+ it('renders a design system BEM class name', () => {
+ render()
+ const component = screen.getByRole('navigation')
+ expect(component).toHaveClass('amsterdam-breadcrumb')
+ })
+
+ it('renders an additional class name', () => {
+ render()
+ const component = screen.getByRole('navigation')
+ expect(component).toHaveClass('amsterdam-breadcrumb extra')
+ })
+
it('renders Breadcrumb component with children', () => {
const breadcrumbItems = [
{ label: 'Item 1', href: '/item-1' },
diff --git a/packages/react/src/Breadcrumb/Breadcrumb.tsx b/packages/react/src/Breadcrumb/Breadcrumb.tsx
index 814b4470bc..eb50c94944 100644
--- a/packages/react/src/Breadcrumb/Breadcrumb.tsx
+++ b/packages/react/src/Breadcrumb/Breadcrumb.tsx
@@ -3,8 +3,10 @@
* Copyright (c) 2023 Gemeente Amsterdam
*/
+import { clsx } from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, ForwardRefExoticComponent, HTMLAttributes, PropsWithChildren, RefAttributes } from 'react'
+import { BreadcrumbItem } from './BreadcrumbItem'
export type BreadcrumbProps = PropsWithChildren>
@@ -12,31 +14,15 @@ interface BreadcrumbComponent extends ForwardRefExoticComponent) => {
- return (
-