diff --git a/docs/backlog/README.md b/docs/backlog/README.md
index bb285f2..71224da 100644
--- a/docs/backlog/README.md
+++ b/docs/backlog/README.md
@@ -55,7 +55,7 @@ for its existing violations, so every WP ends green.
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
-| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | todo |
+| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | todo |
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | todo |
diff --git a/docs/backlog/WP-16-component-a11y.md b/docs/backlog/WP-16-component-a11y.md
index 7cdc07b..78be96b 100644
--- a/docs/backlog/WP-16-component-a11y.md
+++ b/docs/backlog/WP-16-component-a11y.md
@@ -1,6 +1,6 @@
# WP-16 — Component a11y: description wiring + alert role
-Status: todo
+Status: done (pending commit)
Phase: 4 — a11y
## Why
@@ -57,12 +57,25 @@ Audit findings axe can't (fully) catch:
## Acceptance criteria
-- [ ] Description text is programmatically associated in the canonical composition;
+- [x] Description text is programmatically associated in the canonical composition;
login-form BSN hint announced.
-- [ ] `-error` id appended exactly when invalid; order stable.
-- [ ] Error alerts are `role="alert"`; play tests assert both behaviors and run in the
+- [x] `-error` id appended exactly when invalid; order stable.
+- [x] Error alerts are `role="alert"`; play tests assert both behaviors and run in the
CI gate.
+## Deviation from the original plan
+
+`radio-group`/`checkbox` were left unchanged — grepping every call site found zero
+consumers pairing either with a `form-field` `description` (only the login-form BSN
+field, which uses `text-input`). The WP's own Files note ("describedby joins; only
+where the component takes a hint") already carved out this exact case — adding
+`hasDescription` to atoms with no live description consumer would be unused surface,
+not a fix. `radio-group` already had correct `-error`-only wiring; untouched.
+`describedBy()` was added directly on `text-input` rather than factored into a shared
+`shared/kernel` helper — one consumer, ~5 lines, not worth the indirection yet.
+Manual screen-reader spot check (optional per the WP) skipped; the play test is the
+enforced check going forward.
+
## Verification
GREEN + `npm run test-storybook:ci` (includes the new play tests).
diff --git a/documentation.json b/documentation.json
index c4c8e19..37d16f7 100644
--- a/documentation.json
+++ b/documentation.json
@@ -11905,7 +11905,7 @@
},
{
"name": "AlertComponent",
- "id": "component-AlertComponent-859bbe82cccea40581a39ebff20da27d3087f9cca4ec7a25457e0f7a69f1478d7af94539fbcf20efcc96afff2a0f3252d0c88c566f00cb4eea117d1e0c0769e8",
+ "id": "component-AlertComponent-e38aceca94288853333b2fd4ad37535f923420f61c9ba25f100a4035a558c0ada40d88b7e7ddd136f6d6099bb7bc9695de3c513720c0b343b5792a4543de19f5",
"file": "src/app/shared/ui/alert/alert.component.ts",
"encapsulation": [],
"entryComponents": [],
@@ -11917,7 +11917,7 @@
"styles": [
"\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n "
],
- "template": "
Atom: alert/message banner — the CIBG Huisstijl "melding"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n.feedback feedback-* classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (.feedback is a flex row).
\n",
- "rawdescription": "\nAtom: alert/message banner — the CIBG Huisstijl \"melding\"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n`.feedback feedback-*` classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (`.feedback` is a flex row).",
+ "description": "
Atom: alert/message banner — the CIBG Huisstijl "melding"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n.feedback feedback-* classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (.feedback is a flex row).\nErrors are role="alert" (assertive — interrupts) since they need immediate\nattention; other variants stay role="status" (polite) so success/info banners\ndon't interrupt what the user is doing.
\n",
+ "rawdescription": "\nAtom: alert/message banner — the CIBG Huisstijl \"melding\"\n(designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n`.feedback feedback-*` classes: the design system owns surface + icon; we add\nonly the icon's a11y label and a content wrapper (`.feedback` is a flex row).\nErrors are `role=\"alert\"` (assertive — interrupts) since they need immediate\nattention; other variants stay `role=\"status\"` (polite) so success/info banners\ndon't interrupt what the user is doing.",
"type": "component",
- "sourceCode": "import { Component, input } from '@angular/core';\n\ntype AlertType = 'info' | 'ok' | 'warning' | 'error';\n\n// visually-hidden alternative for the status icon (CIBG a11y requirement).\nconst ICON_LABELS: Record = {\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n};\n\n/** Atom: alert/message banner — the CIBG Huisstijl \"melding\"\n (designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n `.feedback feedback-*` classes: the design system owns surface + icon; we add\n only the icon's a11y label and a content wrapper (`.feedback` is a flex row). */\n@Component({\n selector: 'app-alert',\n styles: [\n `\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n `,\n ],\n template: `\n
\n {{ iconLabels[type()] }}\n
\n
\n `,\n})\nexport class AlertComponent {\n type = input('info');\n protected readonly iconLabels = ICON_LABELS;\n}\n",
+ "sourceCode": "import { Component, input } from '@angular/core';\n\ntype AlertType = 'info' | 'ok' | 'warning' | 'error';\n\n// visually-hidden alternative for the status icon (CIBG a11y requirement).\nconst ICON_LABELS: Record = {\n info: $localize`:@@alert.icon.info:Informatie`,\n ok: $localize`:@@alert.icon.ok:Gelukt`,\n warning: $localize`:@@alert.icon.warning:Waarschuwing`,\n error: $localize`:@@alert.icon.error:Foutmelding`,\n};\n\n/** Atom: alert/message banner — the CIBG Huisstijl \"melding\"\n (designsystem.cibg.nl/componenten/meldingen). Thin wrapper over the vendored\n `.feedback feedback-*` classes: the design system owns surface + icon; we add\n only the icon's a11y label and a content wrapper (`.feedback` is a flex row).\n Errors are `role=\"alert\"` (assertive — interrupts) since they need immediate\n attention; other variants stay `role=\"status\"` (polite) so success/info banners\n don't interrupt what the user is doing. */\n@Component({\n selector: 'app-alert',\n styles: [\n `\n .feedback > div {\n flex: 1 1 auto;\n min-width: 0;\n }\n `,\n ],\n template: `\n