diff --git a/documentation/storybook.md b/documentation/storybook.md
index b7c0374ecf..107101c91b 100644
--- a/documentation/storybook.md
+++ b/documentation/storybook.md
@@ -19,7 +19,8 @@ We write our documentation in Dutch.
## Best practices for controls
-1. For props offering five options or less, use radio buttons rather than a dropdown. This makes it easier to compare the options. It saves the user a click to select each option and clearly shows all of them up front.
+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 clearly shows all of them 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/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/index.ts b/packages/react/src/index.ts
index 992c789f5c..9df069b981 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -4,6 +4,7 @@
*/
/* Append here */
+export * from './Badge'
export * from './Table'
export * from './MegaMenu'
export * from './IconButton'
diff --git a/proprietary/tokens/src/components/amsterdam/badge.tokens.json b/proprietary/tokens/src/components/amsterdam/badge.tokens.json
new file mode 100644
index 0000000000..0a722c1226
--- /dev/null
+++ b/proprietary/tokens/src/components/amsterdam/badge.tokens.json
@@ -0,0 +1,49 @@
+{
+ "amsterdam": {
+ "badge": {
+ "font-family": { "value": "{amsterdam.typography.font-family}" },
+ "font-weight": { "value": "{amsterdam.typography.font-weight.bold}" },
+ "padding-inline": { "value": "0.5rem" },
+ "spacious": {
+ "font-size": { "value": "{amsterdam.typography.spacious.text-level.5.font-size}" },
+ "line-height": { "value": "{amsterdam.typography.spacious.text-level.5.line-height}" }
+ },
+ "compact": {
+ "font-size": { "value": "{amsterdam.typography.compact.text-level.5.font-size}" },
+ "line-height": { "value": "{amsterdam.typography.compact.text-level.5.line-height}" }
+ },
+ "blue": {
+ "background-color": { "value": "{amsterdam.color.blue}" },
+ "color": { "value": "{amsterdam.color.primary-black}" }
+ },
+ "dark-blue": {
+ "background-color": { "value": "{amsterdam.color.primary-blue}" },
+ "color": { "value": "{amsterdam.color.primary-white}" }
+ },
+ "dark-green": {
+ "background-color": { "value": "{amsterdam.color.dark-green}" },
+ "color": { "value": "{amsterdam.color.primary-white}" }
+ },
+ "green": {
+ "background-color": { "value": "{amsterdam.color.green}" },
+ "color": { "value": "{amsterdam.color.primary-black}" }
+ },
+ "magenta": {
+ "background-color": { "value": "{amsterdam.color.magenta}" },
+ "color": { "value": "{amsterdam.color.primary-white}" }
+ },
+ "orange": {
+ "background-color": { "value": "{amsterdam.color.orange}" },
+ "color": { "value": "{amsterdam.color.primary-black}" }
+ },
+ "purple": {
+ "background-color": { "value": "{amsterdam.color.purple}" },
+ "color": { "value": "{amsterdam.color.primary-white}" }
+ },
+ "yellow": {
+ "background-color": { "value": "{amsterdam.color.yellow}" },
+ "color": { "value": "{amsterdam.color.primary-black}" }
+ }
+ }
+ }
+}
diff --git a/storybook/storybook-react/src/Accordion/Accordion.stories.tsx b/storybook/storybook-react/src/Accordion/Accordion.stories.tsx
index 3108a2106e..6b2cc639ee 100644
--- a/storybook/storybook-react/src/Accordion/Accordion.stories.tsx
+++ b/storybook/storybook-react/src/Accordion/Accordion.stories.tsx
@@ -24,11 +24,15 @@ const meta = {
},
},
headingLevel: {
- control: { type: 'select' },
+ control: {
+ type: 'radio',
+ },
options: [1, 2, 3, 4],
},
section: {
- control: { type: 'boolean' },
+ control: {
+ type: 'boolean',
+ },
},
},
} satisfies Meta
diff --git a/storybook/storybook-react/src/Badge/Badge.docs.mdx b/storybook/storybook-react/src/Badge/Badge.docs.mdx
new file mode 100644
index 0000000000..2a21267d46
--- /dev/null
+++ b/storybook/storybook-react/src/Badge/Badge.docs.mdx
@@ -0,0 +1,15 @@
+import { Controls, Markdown, Meta, Primary } from "@storybook/blocks";
+import * as BadgeStories from "./Badge.stories.tsx";
+import README from "../../../../packages/css/src/components/badge/README.md?raw";
+
+
+
+{README}
+
+## Stories
+
+### Default
+
+
+
+
diff --git a/storybook/storybook-react/src/Badge/Badge.stories.tsx b/storybook/storybook-react/src/Badge/Badge.stories.tsx
new file mode 100644
index 0000000000..130951da4d
--- /dev/null
+++ b/storybook/storybook-react/src/Badge/Badge.stories.tsx
@@ -0,0 +1,30 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright (c) 2024 Gemeente Amsterdam
+ */
+
+import { Badge } from '@amsterdam/design-system-react'
+import { Meta, StoryObj } from '@storybook/react'
+
+const meta = {
+ title: 'Feedback/Badge',
+ component: Badge,
+ args: {
+ label: 'Tip',
+ },
+ argTypes: {
+ color: {
+ control: {
+ type: 'select',
+ },
+ options: ['blue', 'dark-blue', 'dark-green', 'green', 'magenta', 'orange', 'purple', 'yellow'],
+ selected: 'dark-green',
+ },
+ },
+} satisfies Meta
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {}
diff --git a/storybook/storybook-react/src/Grid/Grid.stories.tsx b/storybook/storybook-react/src/Grid/Grid.stories.tsx
index f6af49f610..12af326926 100644
--- a/storybook/storybook-react/src/Grid/Grid.stories.tsx
+++ b/storybook/storybook-react/src/Grid/Grid.stories.tsx
@@ -18,7 +18,9 @@ const argTypes = {
const gridArgTypes = {
compact: {
- control: { type: 'boolean' },
+ control: {
+ type: 'boolean',
+ },
},
gapVertical: {
control: {
@@ -34,14 +36,24 @@ const gridArgTypes = {
const gridCellArgTypes = {
as: {
- control: { type: 'inline-radio' },
+ control: {
+ type: 'radio',
+ },
options: ['article', 'div', 'section'],
},
span: {
- control: { type: 'number', min: 1, max: 12 },
+ control: {
+ type: 'number',
+ min: 1,
+ max: 12,
+ },
},
start: {
- control: { type: 'number', min: 1, max: 12 },
+ control: {
+ type: 'number',
+ min: 1,
+ max: 12,
+ },
},
}
diff --git a/storybook/storybook-react/src/Header/Header.stories.tsx b/storybook/storybook-react/src/Header/Header.stories.tsx
index f269d04b37..15f1bc5297 100644
--- a/storybook/storybook-react/src/Header/Header.stories.tsx
+++ b/storybook/storybook-react/src/Header/Header.stories.tsx
@@ -13,7 +13,7 @@ const meta = {
argTypes: {
logoBrand: {
control: {
- type: 'select',
+ type: 'radio',
},
options: ['amsterdam', 'ggd-amsterdam', 'stadsarchief', 'stadsbank-van-lening', 'vga-verzekeringen'],
},
diff --git a/storybook/storybook-react/src/Icon/Icon.stories.tsx b/storybook/storybook-react/src/Icon/Icon.stories.tsx
index d7a2b0cc4c..802db0ccf7 100644
--- a/storybook/storybook-react/src/Icon/Icon.stories.tsx
+++ b/storybook/storybook-react/src/Icon/Icon.stories.tsx
@@ -12,14 +12,20 @@ const meta = {
component: Icon,
argTypes: {
size: {
- control: { type: 'select' },
+ control: {
+ type: 'radio',
+ },
options: ['level-3', 'level-4', 'level-5', 'level-6'],
},
square: {
- control: { type: 'boolean' },
+ control: {
+ type: 'boolean',
+ },
},
svg: {
- control: { type: 'select' },
+ control: {
+ type: 'select',
+ },
options: Object.keys(Icons),
mapping: Icons,
},
diff --git a/storybook/storybook-react/src/IconButton/IconButton.stories.tsx b/storybook/storybook-react/src/IconButton/IconButton.stories.tsx
index 19e887e650..abb9b0ba19 100644
--- a/storybook/storybook-react/src/IconButton/IconButton.stories.tsx
+++ b/storybook/storybook-react/src/IconButton/IconButton.stories.tsx
@@ -31,7 +31,9 @@ const meta = {
options: ['level-3', 'level-4', 'level-5', 'level-6'],
},
svg: {
- control: { type: 'select' },
+ control: {
+ type: 'select',
+ },
options: Object.keys(Icons),
mapping: Icons,
},
diff --git a/storybook/storybook-react/src/Link/Link.stories.tsx b/storybook/storybook-react/src/Link/Link.stories.tsx
index c058718a02..44d37aaff2 100644
--- a/storybook/storybook-react/src/Link/Link.stories.tsx
+++ b/storybook/storybook-react/src/Link/Link.stories.tsx
@@ -14,7 +14,9 @@ const meta = {
component: Link,
argTypes: {
icon: {
- control: { type: 'select' },
+ control: {
+ type: 'select',
+ },
options: Object.keys(Icons),
mapping: Icons,
table: {
@@ -22,7 +24,10 @@ const meta = {
},
},
onBackground: {
- control: { type: 'select', labels: { undefined: 'default', light: 'light', dark: 'dark' } },
+ control: {
+ type: 'radio',
+ labels: { undefined: 'default', light: 'light', dark: 'dark' },
+ },
options: [undefined, 'light', 'dark'],
table: {
category: 'API',
@@ -30,7 +35,7 @@ const meta = {
},
variant: {
control: {
- type: 'select',
+ type: 'radio',
labels: { standalone: 'standalone', inline: 'inline', inList: 'inList' },
},
options: ['standalone', 'inline', 'inList'],
diff --git a/storybook/storybook-react/src/Logo/Logo.stories.tsx b/storybook/storybook-react/src/Logo/Logo.stories.tsx
index de319d64ca..634e024fbb 100644
--- a/storybook/storybook-react/src/Logo/Logo.stories.tsx
+++ b/storybook/storybook-react/src/Logo/Logo.stories.tsx
@@ -12,7 +12,7 @@ const meta = {
argTypes: {
brand: {
control: {
- type: 'select',
+ type: 'radio',
},
options: ['amsterdam', 'ggd-amsterdam', 'stadsarchief', 'stadsbank-van-lening', 'vga-verzekeringen'],
},
diff --git a/storybook/storybook-react/src/Paragraph/Paragraph.stories.tsx b/storybook/storybook-react/src/Paragraph/Paragraph.stories.tsx
index d52efb3473..c73887c059 100644
--- a/storybook/storybook-react/src/Paragraph/Paragraph.stories.tsx
+++ b/storybook/storybook-react/src/Paragraph/Paragraph.stories.tsx
@@ -19,7 +19,7 @@ const meta = {
argTypes: {
size: {
control: {
- type: 'select',
+ type: 'radio',
labels: { undefined: 'default', large: 'large', small: 'small' },
},
options: [undefined, 'large', 'small'],
diff --git a/storybook/storybook-react/src/Spotlight/Spotlight.stories.tsx b/storybook/storybook-react/src/Spotlight/Spotlight.stories.tsx
index b106b0af84..fc6427a8c4 100644
--- a/storybook/storybook-react/src/Spotlight/Spotlight.stories.tsx
+++ b/storybook/storybook-react/src/Spotlight/Spotlight.stories.tsx
@@ -14,13 +14,14 @@ const meta = {
component: Spotlight,
argTypes: {
as: {
- control: { type: 'inline-radio' },
+ control: {
+ type: 'radio',
+ },
options: ['article', 'aside', 'div', 'footer', 'section'],
},
color: {
- options: ['blue', 'dark-green', 'green', 'light-blue', 'magenta', 'orange', 'purple', 'yellow'],
control: {
- type: 'radio',
+ type: 'select',
labels: {
blue: 'Blauw',
'dark-green': 'Donkergroen',
@@ -32,6 +33,7 @@ const meta = {
yellow: 'Geel',
},
},
+ options: ['blue', 'dark-green', 'green', 'light-blue', 'magenta', 'orange', 'purple', 'yellow'],
},
children: {
control: {