Skip to content

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 ​

IDRule
FORM-01Every input has a programmatically associated <label>. Placeholder is never the only label.
FORM-02Credential fields set autocomplete: username, current-password, new-password, email. Password managers must be able to generate and save.
FORM-03Password fields offer a reveal toggle (type="button", aria-pressed, accessible name).
FORM-04Constraints (min length, format, allowed values) are shown before the user types, not only on violation.
FORM-05Submit buttons stay enabled while invalid; validation messages explain the block. Disabled submits give the user nothing to act on.
FORM-06Submit 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-07Confirmation fields validate against their source in real time and show affirmative match feedback, not only mismatch errors.
FORM-08Nothing is inserted into the tab order between a field and its submit button. Secondary links go after the submit.
FORM-09Values the user has already supplied are carried forward across steps, never retyped.
FORM-10Paste is never blocked on any field, especially passwords.
FORM-11The first field of a single-purpose form is focused on mount.

MSG — errors, status and feedback ​

IDRule
MSG-01Error 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-02Raw provider/SDK error strings never reach users. Map known codes; fall back to one generic human sentence.
MSG-03Error text says what to do next, not only what went wrong.
MSG-04A dead end always offers a route out — an expired token links to the form that reissues it, not just "back".
MSG-05Destructive or irreversible actions confirm first, and say what will be lost.
IDRule
NAV-01No 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-02Any timer, interval or subscription is cleaned up on unmount.
NAV-03Each route sets a distinct document title.
NAV-04Pages 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-05A 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) ​

IDRule
A11Y-01Text 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-02Interactive targets ≥ 24×24 CSS px (WCAG 2.5.8). Small faint text links fail this routinely.
A11Y-03Every interactive element is keyboard reachable and operable, with a visible focus indicator.
A11Y-04Content sits in landmarks; each page has exactly one <h1>, and heading levels do not skip.
A11Y-05Icon-only controls have an accessible name.
A11Y-06Layout survives a short viewport (≈700px height) and a mobile keyboard being open.

SEC — user-facing security behaviour ​

IDRule
SEC-01Auth responses never reveal whether an account exists — same copy, same timing, for known and unknown addresses.
SEC-02Single-use tokens are verified before rendering the form they gate.
SEC-03A password change invalidates other sessions, and that is enforced server-side (e.g. verifyIdToken(token, true)), not assumed.
SEC-04Security-relevant account changes notify the user out of band.
SEC-05Minimum password length ≥ 8 (NIST 800-63B). Prefer a strength meter over composition rules.

CONTENT — copy ​

IDRule
CONTENT-01All user-facing copy goes through i18n, with parity across every supported locale.
CONTENT-02The heading of a destination echoes the control that led there.
CONTENT-03Confirmation states say what happens next, including the failure path ("check your spam folder").
CONTENT-04Waiting states name what is being waited on.

Per-project notes ​

ProjectStackNotes
playoutFormKit + @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-portalPrimeVue, web + mobile appsPrimeVue supplies Password with a built-in toggle and strength meter — prefer it over hand-rolling FORM-03/SEC-05.
tt-time-trackerPrimeVue 4 + Tailwind 4 + TanStack TableCorrected 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, TailwindCheck 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_accessibility tool 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.