Regulated forms for a global medical information portal
Country and language specific sites where patients and healthcare professionals search regulated product information and submit legally mandated safety reports: adverse events, product complaints, temperature excursions, and medical inquiries. Pharmacovigilance capture to ICH E2B-style requirements, which means the forms are legal instruments, not just UI.
A form engine defined by markup
Non-developers had to be able to compose and reorder multi-step regulated forms per country without a frontend release. Rather than a JSON config, the engine infers its state machine from the authored DOM: each authored section is a step, each nested form a substep, and components self-locate at runtime, so authors can add, remove, and reorder steps freely. Step position serialises to the URL as an encoded hash (position only, never form data, so no PII in URLs), making forms refresh-safe and deep-linkable. Conditional branching lives in the authored markup too, down to a four-level image-driven defect localisation flow for a complaint path involving an implant device.
Submission hardening included a CSRF token service, reCAPTCHA lifecycle management across substeps, and presigned S3 direct file uploads with enforced limits (the continue button disables when the aggregate cap is hit, so the limit is enforced rather than just displayed).
Accessibility where the library falls short
No component library provided compliant widgets for the required patterns, so I built the full WAI-ARIA APG behaviour by hand, including retrofitting semantics onto library internals that don't expose them. Two pieces I'd highlight:
- An accessible combobox (~530 lines) rebuilt over Vuetify's autocomplete, which renders its menu in a detached portal with no ARIA at all. Active-descendant tracking reconciled from four event sources, roles injected into the portal on every open, set-size and position semantics computed after item hoisting so ARIA order matches visual order, dual live regions split by politeness, and duplicate-announcement suppression.
- An ARIA grid date picker (~915 lines) built from scratch: three views (days, months, years), a complete keyboard map, RTL-aware weekday labels, a focus trap, a live announcer with timer hygiene, and an "unknown day" mode that outputs month and year only, because partially known batch and expiry dates are a regulatory reality.
A favourite detail: screen readers won't re-announce an identical message, so the announcer appends a rotating one-to-three zero-width-space suffix to make each string unique.
The Vue 3 rewrite
IE11 end-of-support triggered a fresh 1.x stack: Vue 3, TypeScript strict, Pinia, Tailwind, and Headless UI, mounted as islands into AEM-rendered markup so the CMS keeps authoring, i18n, legal copy, and CSRF. The validated regulated forms stayed frozen on the 0.x stack, deliberate risk isolation rather than a big-bang rewrite. New work included a multi-select product search where the "can't find your product" CTA is injected into the listbox as a keyboard-reachable option, audience gating with a legal confirmation modal, and a typed analytics layer that resolves cookie consent per event rather than once at init.
- Vue 2
- Vue 3
- TypeScript
- Vuex
- Pinia
- Vuetify
- Tailwind
- Headless UI
- Lit
- GraphQL
- AEM
- Cypress
- Playwright
- Storybook
- Chromatic
Back to selected work · Previous: clinical trial finder · Next: design system migration · See the engagement overview · Talk to me about a similar project