Appearance
UX Baseline
The shared behaviour standard for playout, customer-portal, tt-time-tracker and members. Every /ux-audit finding cites a rule ID from this file, so the same defect reads the same way in four different trackers.
Why rules, not components
The four projects are all Vue 3 but use four different component libraries — FormKit + @playout/ui, PrimeVue, Flowbite, @bcc-code/component-library-vue. Consistency therefore cannot mean shared components. It means the same observable behaviour, implemented in whatever each stack provides.
A rule belongs here only if it is (a) checkable by reading code and (b) true regardless of component library.
Status
Seeded from the UX Patterns corpus (authentication/password-reset, forms/password) and from the playout password-reset audit of 2026-07-30, where each rule below was confirmed against real code. Rules get added as audits find recurring defects — a finding seen in two projects should be promoted to a rule.
FORM — forms and input
| ID | Rule |
|---|---|
| FORM-01 | Every input has a programmatically associated <label>. Placeholder is never the only label. |
| FORM-02 | Credential fields set autocomplete: username, current-password, new-password, email. Password managers must be able to generate and save. |
| FORM-03 | Password fields offer a reveal toggle (type="button", aria-pressed, accessible name). |
| FORM-04 | Constraints (min length, format, allowed values) are shown before the user types, not only on violation. |
| FORM-05 | Submit buttons stay enabled while invalid; validation messages explain the block. Disabled submits give the user nothing to act on. |
| FORM-06 | Submit buttons are not disabled during submission either — disabling a just-activated button drops focus to <body>. Use aria-busy and guard in the handler. |
| FORM-07 | Confirmation fields validate against their source in real time and show affirmative match feedback, not only mismatch errors. |
| FORM-08 | Nothing is inserted into the tab order between a field and its submit button. Secondary links go after the submit. |
| FORM-09 | Values the user has already supplied are carried forward across steps, never retyped. |
| FORM-10 | Paste is never blocked on any field, especially passwords. |
| FORM-11 | The first field of a single-purpose form is focused on mount. |
MSG — errors, status and feedback
| ID | Rule |
|---|---|
| MSG-01 | Error blocks carry role="alert"; success and step changes carry role="status" or aria-live="polite". A silent DOM swap tells a screen-reader user nothing (WCAG 4.1.3). |
| MSG-02 | Raw provider/SDK error strings never reach users. Map known codes; fall back to one generic human sentence. |
| MSG-03 | Error text says what to do next, not only what went wrong. |
| MSG-04 | A dead end always offers a route out — an expired token links to the form that reissues it, not just "back". |
| MSG-05 | Destructive or irreversible actions confirm first, and say what will be lost. |
NAV — navigation and page state
| ID | Rule |
|---|---|
| NAV-01 | No timed automatic redirects. A setTimeout(router.push) is WCAG 2.2.1 (Timing Adjustable) and is always too fast to read. Offer an explicit control. |
| NAV-02 | Any timer, interval or subscription is cleaned up on unmount. |
| NAV-03 | Each route sets a distinct document title. |
| NAV-04 | Pages whose URL carries a token or secret set noindex and a no-referrer policy, enforced by response header, not only a runtime meta tag. |
| NAV-05 | A multi-step flow exposes its steps as real states; a step the user must read is not replaced automatically. |
A11Y — accessibility floor (WCAG 2.2 AA)
| ID | Rule |
|---|---|
| A11Y-01 | Text contrast ≥ 4.5:1 (≥ 3:1 for large text). Verify with a contrast tool — no automated code checker can determine this from class names. |
| A11Y-02 | Interactive targets ≥ 24×24 CSS px (WCAG 2.5.8). Small faint text links fail this routinely. |
| A11Y-03 | Every interactive element is keyboard reachable and operable, with a visible focus indicator. |
| A11Y-04 | Content sits in landmarks; each page has exactly one <h1>, and heading levels do not skip. |
| A11Y-05 | Icon-only controls have an accessible name. |
| A11Y-06 | Layout survives a short viewport (≈700px height) and a mobile keyboard being open. |
SEC — user-facing security behaviour
| ID | Rule |
|---|---|
| SEC-01 | Auth responses never reveal whether an account exists — same copy, same timing, for known and unknown addresses. |
| SEC-02 | Single-use tokens are verified before rendering the form they gate. |
| SEC-03 | A password change invalidates other sessions, and that is enforced server-side (e.g. verifyIdToken(token, true)), not assumed. |
| SEC-04 | Security-relevant account changes notify the user out of band. |
| SEC-05 | Minimum password length ≥ 8 (NIST 800-63B). Prefer a strength meter over composition rules. |
CONTENT — copy
| ID | Rule |
|---|---|
| CONTENT-01 | All user-facing copy goes through i18n, with parity across every supported locale. |
| CONTENT-02 | The heading of a destination echoes the control that led there. |
| CONTENT-03 | Confirmation states say what happens next, including the failure path ("check your spam folder"). |
| CONTENT-04 | Waiting states name what is being waited on. |
Per-project notes
| Project | Stack | Notes |
|---|---|---|
| playout | FormKit + @playout/ui, Tailwind, vue-i18n (en/fr/no) | Locale parity is CI-enforced via pnpm check:locales. SecretField is for API secrets, not credentials — use PasswordField for FORM-03. |
| customer-portal | PrimeVue, web + mobile apps | PrimeVue supplies Password with a built-in toggle and strength meter — prefer it over hand-rolling FORM-03/SEC-05. |
| tt-time-tracker | PrimeVue 4 + Tailwind 4 + TanStack Table | Corrected 2026-07-30: the NestJS rework (bb3238c) completed the Flowbite→PrimeVue migration — flowbite is gone from services/client/package.json and no source file references it. MIGRATION-PRIMEVUE.md is the old plan, not current state. As with customer-portal, prefer PrimeVue's Password over hand-rolling FORM-03/SEC-05. |
| members | @bcc-code/component-library-vue, Tailwind | Check whether the shared library already satisfies a rule before filing against this repo — the fix may belong upstream. |
Verification notes
Two things cannot be settled by reading code, and a finding that claims them without evidence is not credible:
- A11Y-01 contrast — needs computed colour values. The UX Patterns
check_accessibilitytool reports contrast as "passed" from markup alone; that result is meaningless. - A11Y-06 / responsive — needs a rendered viewport.
Mark these unverified in a draft rather than asserting them.
CONTENT-01 and the single-locale projects
members and tt-time-tracker have no i18n layer at all — UI copy is French string literals in templates, and that is the documented design in each repo's own guide, not an oversight. CONTENT-01 therefore fails identically on every feature in both projects.
Do not raise it per feature: 56 copies of the same finding is noise that buries the real ones. Treat it as one architectural finding per project, to be raised once against the project as a whole. Individual feature drafts should record CONTENT-01 as not-applicable — see project-level i18n finding. Copy-quality findings (CONTENT-02/03/04) still apply normally in French.
playout (en/fr/no, CI-enforced via pnpm check:locales) and customer-portal (en/nb) are held to CONTENT-01 per feature as written.