# UI/UX audit — BIG-register wizard (Rijkshuisstijl alignment)

Phase A deliverable. Audits the registration wizard and the shared atoms/chrome it
relies on against the project's design system (`@rijkshuisstijl-community`, the NL Design
System Rijkshuisstijl theme — the canonical system here; CIBG's own DS is not installed)
and WCAG 2.1 AA. Each finding maps to the token / component / pattern that resolves it.
This is an **alignment, not a redesign**: fixes go through the token/theme layer and
existing components; no new libraries, no restricted Rijksoverheid assets.

Scope: registratie wizard + shared atoms/chrome (intake/herregistratie inherit the shared
fixes). Their own step copy/inline styles are out of scope.

Priority: **H** = blocks "professional/accessible" bar (a11y or broken token); **M** =
visible inconsistency; **L** = polish.

---

## 1. Tokens vs. hardcoded values

The app imports RHC design tokens but the wizard/chrome were written with **raw inline
values** (≈48 in wizard+chrome, ≈16 in atoms). RHC exposes a full token set we should map
onto: spacing `--rhc-space-max-{sm..5xl}`, type `--rhc-text-font-size-*` /
`--rhc-text-font-weight-*`, color `--rhc-color-foreground-{default,subtle}` /
`--rhc-color-{lintblauw,donkerblauw,cool-grey}-*` / `--rhc-color-wit`, radius
`--rhc-border-radius-*`.

| #   | Pri   | Finding                                                                                                                                                                                  | Resolves to                                                                                      |
| --- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 1.1 | **H** | `var(--rhc-color-grijs-700)` is referenced in 3 wizards but **`--rhc-color-grijs-*` does not exist** in the package → the "Stap X van Y" text falls back to the default color, not grey. | `--rhc-color-foreground-subtle`                                                                  |
| 1.2 | **H** | `site-header`/`site-footer` set bg to `var(--rhc-color-lintblauw-700,#154273)` / `…-900,#01689b` — `lintblauw-900` doesn't exist (only 50–700), so the **hex fallback renders**.         | header `--rhc-color-lintblauw-700`; footer `--rhc-color-donkerblauw-700`; text `--rhc-color-wit` |
| 1.3 | M     | Raw spacing everywhere: `0.25/0.5/0.75/1/1.5/2/3rem` as `gap`/`margin`/`padding` (≈30 occurrences).                                                                                      | `--rhc-space-max-{sm,md,lg,xl,2xl,3xl,5xl}`                                                      |
| 1.4 | M     | Inconsistent form/content widths: `28rem`, `30rem`, `32rem`, `64rem` as raw `max-width`.                                                                                                 | app width tokens in `styles.scss` (`--app-form-max`, `--app-content-max`) mapped once            |
| 1.5 | M     | `spinner` & `skeleton` hardcode greys/accent (`#cad0d6`, `#e8ebee`, `#f3f5f6`) and the spinner accent via bogus `--rhc-color-lintblauw-700,#154273` fallback.                            | `--rhc-color-cool-grey-{200,300}`, `--rhc-color-lintblauw-700`                                   |
| 1.6 | L     | `site-header` hardcodes `font-weight:700/400`, `font-size:0.9rem`, `opacity:0.85`.                                                                                                       | `--rhc-text-font-weight-{bold,regular}`, `--rhc-text-font-size-sm`                               |

## 2. Typography & heading hierarchy

| #   | Pri   | Finding                                                                                                                                                              | Resolves to                                   |
| --- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| 2.1 | **H** | Each page has one `<h1>` (page-shell) but the **wizard steps have no `<h2>`** — step title is a plain `<p>`. Screen-reader users get no step heading to navigate to. | per-step `<h2>` via `app-heading [level]="2"` |
| 2.2 | M     | Type scale not applied to bespoke text (header wordmark uses raw sizes).                                                                                             | `--rhc-text-font-size-*`, `app-heading`       |

## 3. Color & contrast

| #   | Pri   | Finding                                                                                                                                                                     | Resolves to                                                      |
| --- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 3.1 | **H** | Because of 1.1/1.2 the actual rendered colors are partly accidental (default text color, hex fallbacks) — contrast is unverified.                                           | map to real tokens, then verify AA                               |
| 3.2 | M     | Palette discipline: confirm a single primary blue (lintblauw) + neutrals + status-only accents (`alert` types ok/info/warning/error already map to Utrecht alert variants). | keep alert variants; route blues to lintblauw/donkerblauw tokens |

## 4. Spacing & layout

| #   | Pri | Finding                                                                                                                                          | Resolves to                                                                   |
| --- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| 4.1 | M   | Repeated inline idioms: `display:flex;gap:0.5rem;margin-top:1rem` (button rows, 6×), `max-width:30rem` (forms, 3×), `margin:1rem 0` (summaries). | utility classes `.app-button-row`, `.app-form`, `.app-stack` in `styles.scss` |
| 4.2 | L   | `shell` uses a custom `--app-content-max:64rem` defined inline.                                                                                  | promote to the `styles.scss` token layer                                      |

## 5. Component reuse

| #   | Pri | Finding                                                                                                                 | Resolves to           |
| --- | --- | ----------------------------------------------------------------------------------------------------------------------- | --------------------- |
| 5.1 | L   | `intake-wizard` review step uses bespoke `<div><dt><dd>` instead of `app-data-row` (out of scope here; note for later). | `app-data-row`        |
| 5.2 | M   | Button rows / form containers are ad-hoc inline layout rather than a shared idiom.                                      | utility classes (4.1) |

## 6. Form, validation & status patterns

| #   | Pri   | Finding                                                                                                                                                        | Resolves to                                                                                |
| --- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| 6.1 | **H** | `form-field` renders the error with `role="alert"` but **no `id`**, and the projected input has **no `aria-describedby`** → error not programmatically linked. | error `id="${fieldId}-error"`; input `aria-describedby`                                    |
| 6.2 | **H** | `text-input` never sets `aria-invalid` even when `invalid()` is true.                                                                                          | `[attr.aria-invalid]`                                                                      |
| 6.3 | **H** | `radio-group` has `role="radiogroup"` but **no accessible name** and no invalid state.                                                                         | `aria-labelledby="${name}-label"`, add `invalid` input → `aria-invalid`/`aria-describedby` |
| 6.4 | M     | Async states (`app-async`) render but aren't announced (no live region) — SR users miss loading→loaded/empty/error.                                            | wrap in `aria-live="polite"` + `aria-busy`                                                 |
| 6.5 | L     | No error-summary pattern; per-field inline errors only. Acceptable for short steps; revisit if steps grow.                                                     | (defer)                                                                                    |

## 7. Page chrome

| #   | Pri | Finding                                                                                | Resolves to                                                          |
| --- | --- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| 7.1 | M   | No breadcrumb / location context in the portal chrome.                                 | new `app-breadcrumb` (`.rhc-breadcrumb-nav`), wired via `page-shell` |
| 7.2 | M   | Step progress is non-semantic text.                                                    | new `app-stepper` (`<ol>`, `aria-current="step"`)                    |
| 7.3 | L   | Skip-link uses `left:-999px` (works) and landmarks (`header/main/footer`) are correct. | keep; tokenize offset                                                |

## 8. Copy & tone

| #   | Pri | Finding                                                                                                                           | Resolves to                                                                                   |
| --- | --- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| 8.1 | M   | Microcopy is functional but informal/uneven (labels, helper text, button text, the manual-diploma warning, the controle summary). | formal, plain official Dutch; consistent with domain terms (registratie wizard + chrome only) |

## 9. Accessibility (WCAG 2.1 AA) — consolidated

| #   | Pri   | Finding                                                                                                         | Resolves to                                                                      |
| --- | ----- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| 9.1 | **H** | No focus management: after Next/Back focus stays on the button; SR/keyboard users aren't moved to the new step. | move focus to the step `<h2>` (`tabindex="-1"`) on `cursor` change               |
| 9.2 | **H** | Error/label/invalid association missing (6.1–6.3).                                                              | as above                                                                         |
| 9.3 | M     | Step changes & async states not announced (6.4).                                                                | `aria-live`                                                                      |
| 9.4 | M     | `skeleton` placeholders are announced as content.                                                               | `aria-hidden="true"`                                                             |
| 9.5 | L     | No automated a11y in CI; only Storybook `addon-a11y` (axe) exists.                                              | add a11y `parameters` in `preview.ts`; keep manual keyboard/SR pass; no new deps |

## 10. Responsive

| #    | Pri | Finding                                                                                  | Resolves to                                             |
| ---- | --- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| 10.1 | M   | Fixed `rem` widths and inline flex rows; verify reflow + ≥24px targets at narrow widths. | token widths + `flex-wrap` on button rows; manual check |

---

## Tooling note

No eslint/stylelint/axe-core in the repo, and the prime directive forbids adding
dependencies. Token compliance is enforced by a **no-dep `check:tokens` npm script**
(greps touched templates for raw `#hex` and bare `rem/px` outside `var(...)`);
accessibility uses the already-installed **Storybook `addon-a11y`** plus a manual
keyboard/screen-reader pass.
