skills/magucc/amsterdam-agent-skills/amsterdam-design-system

amsterdam-design-system

SKILL.md

Amsterdam Design System

Production guidance for building City of Amsterdam digital products using the official design system. Components, tokens, layout patterns, and integration with Tailwind CSS v4.

Docs: https://designsystem.amsterdam/ Repo: https://github.com/Amsterdam/design-system Storybook: https://storybook.designsystem.amsterdam/

Overview

The design system ships as 5 npm packages:

Package Purpose
@amsterdam/design-system-assets Amsterdam Sans font files
@amsterdam/design-system-css BEM component styles (ams-* classes)
@amsterdam/design-system-tokens CSS custom properties (--ams-*) in Spacious + Compact modes
@amsterdam/design-system-react React components (66 components, all with forwardRef)
@amsterdam/design-system-react-icons Icon components for the AMS icon set

No provider or context wrapper required — import CSS, use components.

Setup

Install

npm install @amsterdam/design-system-assets @amsterdam/design-system-css @amsterdam/design-system-react @amsterdam/design-system-react-icons @amsterdam/design-system-tokens

CSS Imports — ORDER MATTERS

// ⚠️ CRITICAL: This exact order is required. Fonts → CSS → Tokens.
import "@amsterdam/design-system-assets/font/index.css"   // 1. Font files
import "@amsterdam/design-system-css/dist/index.css"       // 2. Component styles
import "@amsterdam/design-system-tokens/dist/index.css"    // 3. Design tokens

For compact mode (internal tools), add one more import AFTER tokens:

import "@amsterdam/design-system-tokens/dist/compact.css"  // 4. Compact overrides

Root Element

Add the ams-body class to your body or root element:

// Next.js (app/layout.tsx)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="nl">
      <body className="ams-body">{children}</body>
    </html>
  )
}

Bold Text Fix

Amsterdam Sans uses weight 800 for bold, not the browser default 700. The ams-body class handles this, but if you scope differently:

.your-root {
  font-weight: var(--ams-typography-body-text-font-weight); /* 400 */
}
.your-root strong, .your-root b {
  font-weight: var(--ams-typography-body-text-bold-font-weight); /* 800 */
}

Component Patterns

Simple Components

import { Heading, Paragraph, Button, Alert } from "@amsterdam/design-system-react"

<Heading level={1}>Page Title</Heading>
<Paragraph>Body text uses Amsterdam Sans at 18-20px fluid.</Paragraph>
<Paragraph size="small">Secondary text at 16px.</Paragraph>
<Button variant="primary">Submit</Button>
<Button variant="secondary">Cancel</Button>
<Alert heading="Let op" headingLevel={2} severity="warning">
  Check your input before proceeding.
</Alert>

Compound Components (dot notation)

Many components use Component.SubComponent pattern via Object.assign:

import { Accordion, Grid, Table, Tabs } from "@amsterdam/design-system-react"

{/* Accordion */}
<Accordion headingLevel={2}>
  <Accordion.Section label="Section title">
    <Paragraph>Section content.</Paragraph>
  </Accordion.Section>
</Accordion>

{/* Grid */}
<Grid paddingVertical="large">
  <Grid.Cell span={8}>Main content</Grid.Cell>
  <Grid.Cell span={4}>Sidebar</Grid.Cell>
</Grid>

{/* Table */}
<Table>
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>Name</Table.HeaderCell>
      <Table.HeaderCell>Value</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.Cell>Item</Table.Cell>
      <Table.Cell>100</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table>

{/* Tabs */}
<Tabs>
  <Tabs.List>
    <Tabs.Button aria-controls="tab1">First</Tabs.Button>
    <Tabs.Button aria-controls="tab2">Second</Tabs.Button>
  </Tabs.List>
  <Tabs.Panel id="tab1">First panel content</Tabs.Panel>
  <Tabs.Panel id="tab2">Second panel content</Tabs.Panel>
</Tabs>

Form Field Composition

AMS forms use a composition pattern: Field wraps Label + input + ErrorMessage.

import { Field, Label, TextInput, TextArea, Select, ErrorMessage, Checkbox, Radio, FieldSet } from "@amsterdam/design-system-react"

{/* Text field */}
<Field invalid={hasError}>
  <Label htmlFor="name">Naam</Label>
  <ErrorMessage>Vul uw naam in</ErrorMessage>
  <TextInput id="name" invalid={hasError} />
</Field>

{/* Textarea */}
<Field>
  <Label htmlFor="message">Bericht</Label>
  <TextArea id="message" rows={4} />
</Field>

{/* Select */}
<Field>
  <Label htmlFor="city">Stadsdeel</Label>
  <Select id="city">
    <Select.Option value="centrum">Centrum</Select.Option>
    <Select.Option value="west">West</Select.Option>
    <Select.Option value="oost">Oost</Select.Option>
  </Select>
</Field>

{/* Checkbox/Radio group */}
<FieldSet legend="Voorkeur" invalid={hasError}>
  <Checkbox>Optie A</Checkbox>
  <Checkbox>Optie B</Checkbox>
</FieldSet>

<FieldSet legend="Type">
  <Radio name="type" value="a">Type A</Radio>
  <Radio name="type" value="b">Type B</Radio>
</FieldSet>

Page Layout

import { Grid, Page, PageHeader, PageFooter, PageHeading, Paragraph } from "@amsterdam/design-system-react"

<Page>
  <PageHeader brandName="Mijn Amsterdam" logoLink="/" />

  <Grid paddingVertical="large">
    <Grid.Cell span="all">
      <PageHeading>Welkom</PageHeading>
    </Grid.Cell>
    <Grid.Cell span={8}>
      <Paragraph>Main content area.</Paragraph>
    </Grid.Cell>
    <Grid.Cell span={4}>
      <Paragraph>Sidebar content.</Paragraph>
    </Grid.Cell>
  </Grid>

  <PageFooter>
    <PageFooter.Spotlight>
      <Paragraph>Contact info</Paragraph>
    </PageFooter.Spotlight>
  </PageFooter>
</Page>

Dialog

import { Button, Dialog, Paragraph } from "@amsterdam/design-system-react"

<Button onClick={() => Dialog.open("confirm-dialog")}>Open Dialog</Button>

<Dialog
  id="confirm-dialog"
  heading="Bevestiging"
  footer={
    <>
      <Button variant="primary" onClick={() => Dialog.close()}>Bevestigen</Button>
      <Button variant="secondary" onClick={() => Dialog.close()}>Annuleren</Button>
    </>
  }
>
  <Paragraph>Weet u het zeker?</Paragraph>
</Dialog>

Available Components

Full props reference: read references/components.md

Layout

Grid (.Cell) · Column · Row · Breakout (.Cell) · Overlap · Page · Spotlight

Page Structure

PageHeader (.GridCellNarrowWindowOnly, .MenuLink) · PageFooter (.Menu, .MenuLink, .Spotlight) · PageHeading

Typography

Heading · Paragraph · Blockquote · Link · StandaloneLink · CallToActionLink · Mark

Buttons & Actions

Button · IconButton · ActionGroup

Form Controls

TextInput · TextArea · Select (.Group, .Option) · Checkbox · Radio · Switch · DateInput · TimeInput · PasswordInput · FileInput · SearchField (.Button, .Input) · CharacterCount

Form Structure

Field · FieldSet · Label · Hint · ErrorMessage · InvalidFormAlert

Navigation

Breadcrumb (.Link) · LinkList (.Link) · Menu (.Link) · Pagination · SkipLink · Tabs (.Button, .List, .Panel) · TableOfContents (.Link, .List)

Data Display

Accordion (.Section) · Card (.Heading, .HeadingGroup, .Image, .Link) · DescriptionList (.Description, .Section, .Term) · Figure (.Caption) · Table (.Body, .Caption, .Cell, .Footer, .Header, .HeaderCell, .Row) · ImageSlider

Feedback

Alert · Dialog (.open(), .close()) · Badge · Avatar

Utility

Icon · Logo · FileList (.Item) · OrderedList (.Item) · UnorderedList (.Item) · ProgressList (.Step, .Substep, .Substeps)

Grid System

The AMS grid is responsive with 3 breakpoints:

Breakpoint Columns Viewport Padding
Narrow 4 < 576px --ams-space-l (24-36px)
Medium 8 576px – 1023px --ams-space-xl (36-60px)
Wide 12 ≥ 1024px --ams-space-2xl (48-90px)

Grid.Cell span prop

{/* Fixed span across all breakpoints */}
<Grid.Cell span={6}>Half width on wide</Grid.Cell>

{/* Full width */}
<Grid.Cell span="all">Full width row</Grid.Cell>

{/* Responsive spans: { narrow, medium, wide } */}
<Grid.Cell span={{ narrow: 4, medium: 4, wide: 8 }}>
  Responsive content
</Grid.Cell>

{/* Start position */}
<Grid.Cell span={6} start={4}>Offset cell</Grid.Cell>
<Grid.Cell span={{ narrow: 4, medium: 6, wide: 8 }} start={{ narrow: 1, medium: 2, wide: 3 }}>
  Responsive offset
</Grid.Cell>

Grid props

<Grid
  as="main"                          // Semantic element
  paddingVertical="large"            // Vertical padding: 'large' | 'x-large' | '2x-large'
  gapVertical="large"               // Row gap: 'none' | 'large' | '2x-large'
>

Design Tokens

Full token catalog: read references/tokens.md

The token system uses a 3-layer hierarchy. All tokens are CSS custom properties prefixed with --ams-.

Brand tokens  →  Common tokens  →  Component tokens
(core values)    (shared patterns)  (per-component)

Reference chain example:

Brand:     --ams-color-interactive-default: #004699
Common:    --ams-links-color: var(--ams-color-interactive-default)
Component: --ams-link-color: var(--ams-links-color)

Key Token Categories

Category Prefix Examples
Colors --ams-color- text, text-inverse, text-secondary, background, interactive, interactive-hover, feedback-error, feedback-success, separator
Spacing --ams-space- xs (4-6px), s (8-12px), m (16-24px), l (24-36px), xl (36-60px), 2xl (48-90px) — all fluid clamp()
Typography --ams-typography- font-family ('Amsterdam Sans', Arial, sans-serif), body-text-font-size, body-text-line-height, heading sizes per level
Borders --ams-border-width- s (1px), m (2px), l (3px), xl (4px)
Focus --ams-focus- outline-offset (4px)

Using Tokens in CSS

.my-component {
  color: var(--ams-color-text);
  background: var(--ams-color-background);
  padding: var(--ams-space-m);
  font-family: var(--ams-typography-font-family);
  border: var(--ams-border-width-s) solid var(--ams-color-separator);
}

Using Tokens in JS

import tokens from "@amsterdam/design-system-tokens/dist/index.json"
const primaryColor = tokens.ams.color.interactive.default // "#004699"

Spacious vs Compact

Aspect Spacious (default) Compact
Use for Public websites Internal tools, dashboards
Body text 18-20px fluid 16px fixed
H1 32-48px fluid 24-28px fluid
Line height 1.8 1.5
Space m 16-24px fluid 12-16px fluid
Space 2xl 48-90px fluid 32-48px fluid
Borders Thicker (m=2px, xl=4px) Thinner (m=1px, xl=3px)

Decision rule: Public-facing site → Spacious. Back-office/admin/dashboard → Compact.

Setup difference — one extra import:

// Spacious (default)
import "@amsterdam/design-system-tokens/dist/index.css"

// Compact (add after tokens)
import "@amsterdam/design-system-tokens/dist/index.css"
import "@amsterdam/design-system-tokens/dist/compact.css"

Compact overrides the same CSS custom properties with smaller values. No code changes needed — components adapt automatically.

Router Integration

AMS Link components render <a> by default. For SPA routing, use polymorphic rendering:

Next.js (App Router)

import NextLink from "next/link"
import { Link, Breadcrumb, Pagination } from "@amsterdam/design-system-react"

{/* Regular link */}
<Link href="/about" legacyBehavior passHref>
  <NextLink>Over ons</NextLink>
</Link>

{/* Or simpler: just use Next.js Link with AMS classes */}
<NextLink href="/about" className="ams-link">Over ons</NextLink>

{/* Pagination with router links */}
<Pagination
  totalPages={10}
  page={currentPage}
  linkTemplate={(page) => `/results?page=${page}`}
  linkComponent={NextLink}
/>

{/* PageHeader logo */}
<PageHeader
  brandName="Mijn Amsterdam"
  logoLink="/"
  logoLinkComponent={NextLink}
/>

React Router

import { Link as RouterLink } from "react-router-dom"
import { Link } from "@amsterdam/design-system-react"

<RouterLink to="/about" className="ams-link">Over ons</RouterLink>

Tailwind v4 Integration

Full bridge config: read references/tailwind-bridge.md

When using Tailwind CSS v4 alongside AMS, map AMS tokens to Tailwind's @theme so utilities use the design system values:

/* app.css */
@import "tailwindcss";
@import "@amsterdam/design-system-assets/font/index.css";
@import "@amsterdam/design-system-css/dist/index.css";
@import "@amsterdam/design-system-tokens/dist/index.css";

/* Disable Tailwind's preflight — AMS CSS handles base styles */
@layer base {
  /* AMS body styles take precedence */
}

@theme {
  /* Map AMS spacing */
  --spacing-ams-xs: var(--ams-space-xs);
  --spacing-ams-s: var(--ams-space-s);
  --spacing-ams-m: var(--ams-space-m);
  --spacing-ams-l: var(--ams-space-l);
  --spacing-ams-xl: var(--ams-space-xl);
  --spacing-ams-2xl: var(--ams-space-2xl);

  /* Map AMS colors */
  --color-ams-text: var(--ams-color-text);
  --color-ams-text-secondary: var(--ams-color-text-secondary);
  --color-ams-text-inverse: var(--ams-color-text-inverse);
  --color-ams-bg: var(--ams-color-background);
  --color-ams-interactive: var(--ams-color-interactive);
  --color-ams-interactive-hover: var(--ams-color-interactive-hover);
  --color-ams-error: var(--ams-color-feedback-error);
  --color-ams-success: var(--ams-color-feedback-success);
  --color-ams-warning: var(--ams-color-feedback-warning);
  --color-ams-info: var(--ams-color-feedback-info);
  --color-ams-separator: var(--ams-color-separator);

  /* Map AMS font */
  --font-ams: var(--ams-typography-font-family);
}

Usage rule: Use AMS React components for all standard UI (buttons, forms, headings, grids, etc.). Use Tailwind utilities only for custom layout (flex, positioning) and one-off spacing that AMS components don't cover.

{/* AMS component — always preferred */}
<Button variant="primary">Submit</Button>

{/* Tailwind for custom layout around AMS components */}
<div className="flex items-center gap-ams-m">
  <Icon svg={SearchIcon} />
  <Paragraph>Search results</Paragraph>
</div>

Custom Components

When building components not in the AMS library, follow these conventions:

BEM Naming

/* Block: ams-status-badge */
.ams-status-badge { }
.ams-status-badge--active { }
.ams-status-badge__icon { }
.ams-status-badge__label { }

Token-Only Styling

.ams-status-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--ams-space-xs);
  padding-block: var(--ams-space-xs);
  padding-inline: var(--ams-space-s);
  font-family: var(--ams-typography-font-family);
  font-size: var(--ams-typography-body-text-small-font-size);
  line-height: var(--ams-typography-body-text-small-line-height);
  border: var(--ams-border-width-s) solid var(--ams-color-separator);
  /* NO hardcoded colors, sizes, or spacing */
}

Component Pattern

import { forwardRef } from "react"
import clsx from "clsx"

export interface StatusBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
  status: "active" | "inactive" | "pending"
}

export const StatusBadge = forwardRef<HTMLSpanElement, StatusBadgeProps>(
  ({ status, className, children, ...restProps }, ref) => (
    <span
      ref={ref}
      className={clsx("ams-status-badge", `ams-status-badge--${status}`, className)}
      {...restProps}
    >
      {children}
    </span>
  )
)

StatusBadge.displayName = "StatusBadge"

Checklist for Custom Components

  • Uses forwardRef
  • Extends relevant HTML element attributes
  • Uses clsx for className composition
  • Spreads ...restProps on root element
  • BEM classes with ams- prefix
  • All styling via --ams-* tokens (no hardcoded values)
  • Sets displayName

TypeScript Patterns

Prop Types

// Intersect with HTML attributes
interface MyComponentProps extends React.HTMLAttributes<HTMLDivElement> {
  variant: "primary" | "secondary"
}

// For form elements
interface MyInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  invalid?: boolean
}

Compound Component Export

// Following AMS pattern with Object.assign
const ListRoot = forwardRef<HTMLUListElement, ListProps>(/* ... */)
const ListItem = forwardRef<HTMLLIElement, ListItemProps>(/* ... */)

export const List = Object.assign(ListRoot, { Item: ListItem })
// Usage: <List><List.Item>...</List.Item></List>

Common Mistakes

Mistake Fix
Wrong import order Fonts → CSS → Tokens (always)
Missing ams-body class Add to <body> or root element
Hardcoded colors (#004699) Use var(--ams-color-interactive)
Hardcoded spacing (16px) Use var(--ams-space-m)
Using font-weight: 700 for bold Use 800 or var(--ams-typography-body-text-bold-font-weight)
Setting font-size: 62.5% on html Don't — AMS uses rem values calibrated to 16px base
Missing invalid prop on both Field and input Both <Field invalid> and <TextInput invalid> need it
Using <h1> instead of <Heading level={1}> Always use AMS Heading component
Missing aria-controls on Tabs.Button Required prop — must match Tabs.Panel id
Using Tailwind bg-blue-500 instead of AMS tokens Use bg-ams-interactive or AMS component

Icon Usage

Full icon catalog and naming conventions: read references/icons.md

Icons are visual symbols for quick communication. They must always be wrapped in the Icon component for consistent sizing and alignment. The icon set ships in @amsterdam/design-system-react-icons (345+ icons, v2.0.0+).

Basic Usage

import { Icon, Button, IconButton } from "@amsterdam/design-system-react"
import { SearchIcon, CloseIcon, NotificationIcon } from "@amsterdam/design-system-react-icons"

{/* Standalone decorative icon — hidden from assistive tech by default */}
<Icon svg={SearchIcon} />

{/* Sized to match text */}
<Icon svg={SearchIcon} size="large" />          {/* matches large body text */}
<Icon svg={SearchIcon} size="heading-3" />       {/* matches heading level 3 */}

{/* Inverse color for dark backgrounds */}
<Icon svg={SearchIcon} color="inverse" />

{/* Square bounding box (useful for alignment in grids) */}
<Icon svg={SearchIcon} square />

{/* Button with icon (icon appears after text by default) */}
<Button icon={SearchIcon}>Zoeken</Button>
<Button icon={SearchIcon} iconBefore>Zoeken</Button>

{/* Icon-only button — label is REQUIRED for accessibility */}
<IconButton svg={CloseIcon} label="Sluiten" />

Icon Props

Prop Type Default Description
svg Function | ReactNode required Icon component from the icon package or custom SVG
size 'small' | 'large' | 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4' | 'heading-5' Size aligned to text line heights
color 'inverse' White icon for dark backgrounds
square boolean false Square bounding box

Icons With Other Components

import { StandaloneLink, Badge } from "@amsterdam/design-system-react"
import { SearchIcon, StarIcon } from "@amsterdam/design-system-react-icons"

<StandaloneLink href="/search" icon={SearchIcon}>Zoek op de website</StandaloneLink>
<Badge label="Nieuw" icon={StarIcon} color="azure" />

v2.0.0 Renames (Breaking)

These icons were renamed in v2.0.0 — use the new names:

Old name New name
BellIcon / BellFillIcon NotificationIcon / NotificationFillIcon
PersonCircleIcon / PersonCircleFillIcon UserAccountIcon / UserAccountFillIcon
TrashBinIcon DeleteIcon
CogwheelIcon SettingsIcon
CheckMarkCircleIcon SuccessIcon

Custom SVGs

{/* Must use viewBox="0 0 24 24" and fill="currentColor" */}
<Icon svg={
  <svg viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2L2 7l10 5 10-5-10-5z" />
  </svg>
} />

Guidelines

  • Icons accompany text in buttons and links — standalone icons only for universal conventions (hamburger menu, search, playback controls)
  • Default color: black/white matching container. Interactive state: blue. Disabled: grey
  • Icons align left of text, vertically centered to the first line
  • The Icon component sets hidden on the <span> — icons are decorative by default. For meaningful icons, use IconButton with a label prop
  • WCAG contrast requirements apply to icons same as typography

Reference Files

For detailed API docs, token catalogs, and templates, read the reference files in references/:

  • components.md — Full props and code examples for each component
  • tokens.md — Complete --ams-* token catalog with values for both modes
  • layout-patterns.md — Page layout templates (public site, dashboard, form page)
  • tailwind-bridge.md — Complete Tailwind v4 + AMS integration guide
  • icons.md — Icon catalog from @amsterdam/design-system-react-icons
Weekly Installs
3
GitHub Stars
1
First Seen
Mar 2, 2026
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
kimi-cli3
amp3