ADR 013 — Design system: Ant Design Vue (replacing PrimeNG)
Status
Accepted — the design-system base for the rewrite (ADR 011); the accessibility work it depends on is ADR 015. The Phase-0 spike (see Migration Plan) de-risks SSR style handling early in implementation.
Context
The Angular app used PrimeNG, which moved to non-free licensing for the versions Registry would need — the trigger for replacing the component library. The rewrite (ADR 011) is on Vue 3 / Nuxt, so the replacement must be a Vue library, and it must be genuinely free so the project does not walk into a second licensing trap. Registry is a data-heavy line-of-business admin UI — tables, forms, date/time pickers, pagination, modals — so a comprehensive component set is worth a lot.
The options weighed:
- Ant Design Vue — MIT-licensed, enterprise-grade, very complete component set; theming via design tokens; CSS-in-JS.
- Nuxt UI (on Reka UI) — accessibility-first headless primitives with a styled layer; less "batteries-included" for dense data components, and its Pro tier is paid.
- Reka UI + Tailwind (headless) — WAI-ARIA-first primitives, fully free, but the entire visual layer is hand-built.
- Vuetify — Material, comprehensive, but heavier and less focused on dense enterprise data.
- PrimeVue — excluded: same licensing situation as PrimeNG.
Decision
Adopt Ant Design Vue (MIT) as the component library and design-system base.
- Comprehensive components cover the admin surface (data tables, forms + validation display, pickers, modals, notifications) with little custom building.
- i18n through
ConfigProviderlocale(en/fr), wired to the app's i18n layer.
Theming
Theming is applied through ConfigProvider theme, split across two inputs so brand and mode are independent:
Brand seed tokens —
colorPrimary, the status colors (colorSuccess/Warning/Error),borderRadius, andlogo— come from the runtime config (ADR 008 / ADR 023), so per-environment rebranding stays a deploy-time concern with no rebuild. They map onto Ant Design seed tokens.Light / dark is the user preference (
SYSTEM/LIGHT/DARK, Pinia state — ADR 014), mapped to Ant Design'sdefaultAlgorithm/darkAlgorithm;SYSTEMfollowsprefers-color-scheme.Per-mode brand overrides. Because a primary that reads well in light can be too light or too dark in dark mode, the config allows a
darkoverride block; any token it does not set inherits the base (light) value. The common case stays a single value, while operators can tunecolorPrimary(and, e.g., a dark-modelogo) per mode. This also keeps the WCAG contrast target met in both modes (ADR 015):json"theme": { "colorPrimary": "#1677ff", "colorSuccess": "#52c41a", "borderRadius": 6, "logo": "/brand/logo.svg", "dark": { "colorPrimary": "#3c9aff", "logo": "/brand/logo-dark.svg" } }SSR-resolved. Both the seed tokens and the resolved algorithm are decided on the Nuxt server (runtime config + the theme preference held in the BFF session — ADR 022), so first paint already carries the right brand color and mode — no flash of default-blue or wrong-mode before hydration. Proven in the Phase-0 spike alongside the CSS-in-JS style extraction.
SSR style handling is a spike item. Ant Design Vue uses CSS-in-JS, so server rendering (ADR 012) requires style extraction (
@ant-design/cssinjsstyle register via a Nuxt plugin) to avoid a flash of unstyled content, and the same inline-style mechanism complicates the strict CSP (style-src 'nonce-…') defined in the frontend security headers (ADR 024). The Phase-0 spike de-risks clean SSR extraction and its interaction with the nonced CSP early in implementation.
Brand assets
"Theme" also covers the app's visual identity beyond design tokens — logo, favicon, pictograms / iconography, and status illustrations (error, not-found, forbidden, empty-state, warning, success, offline). This is a second, config-driven layer:
- In-app defaults + per-environment override. The app ships a complete default asset set; the runtime config (
assetsblock) may override any subset, and anything not overridden falls back to the built-in default — so a deployment rebrands only what it wants and an omitted override never leaves a blank screen. - Per-mode variants where it matters (a dark-mode illustration or logo), using the same base /
dark-override shape as the theme tokens. - Accessibility. Each asset carries meaningful
alttext when it conveys information, oraria-hiddenwhen purely decorative — specified with the rest of the a11y work (ADR 015).
"assets": {
"logo": { "light": "/brand/logo.svg", "dark": "/brand/logo-dark.svg" },
"favicon": "/brand/favicon.ico",
"pictograms": { "warning": "/brand/warn.svg" },
"illustrations": {
"error": "/brand/error.svg",
"empty": "/brand/empty.svg",
"notFound": { "light": "/brand/404.svg", "dark": "/brand/404-dark.svg" }
}
}(any missing entry falls back to the built-in default)
Accessibility caveat, stated plainly. Ant Design Vue is not a headless / WAI-ARIA-first library. It gives reasonable defaults, but the accessibility mandate (keyboard navigation, ARIA correctness, focus management, no overlay/menu traps) will require manual hardening and auditing on top of the components — that work is a dedicated workstream in ADR 015, not a byproduct of the library. This is the main trade-off accepted with this choice.
Consequences
Positive
- Genuinely free (MIT). Resolves the licensing problem that triggered the change, with no premium tier required for core components.
- Batteries-included for a data-heavy admin UI. The complete component set minimizes custom component building, which suits Registry's surface.
- Deploy-time brand identity preserved — colors and imagery. Design tokens map to Ant Design tokens, and the brand-asset layer (logo, pictograms, status illustrations) overrides in-app defaults from config, so a tenant/environment can fully rebrand without a rebuild — keeping the one-image, config-at-deploy model.
Negative
- CSS-in-JS + SSR friction. Style extraction is required to avoid FOUC, and inline styles make a strict CSP harder — both flagged for the spike and the deferred CSP work.
- Accessibility is not first-class. The a11y overhaul carries real manual weight — the dedicated ADR 015 — rather than coming free from the library — the opposite of what a headless/ARIA-first base would have given.
- Opinionated visual language and bundle weight. Customization lives within Ant Design's token system, and the full library is not the lightest option.
Why not Nuxt UI / Reka (headless, a11y-first)
A headless, WAI-ARIA-first base would have made much of the accessibility mandate essentially free, which is a strong pull given ADR 015. It was not chosen because it is less batteries-included for dense data components (more to build) and its styled/Pro tiers reintroduce cost questions; the decision favoured Ant Design Vue's complete enterprise component set, accepting the manual a11y cost in exchange.
Why not Vuetify or PrimeVue
Vuetify is comprehensive but Material-flavoured, heavier, and less focused on dense enterprise data. PrimeVue is excluded outright — it carries the same licensing situation as PrimeNG, so it would not solve the problem that started this.