Audit
Inconsistencies, state gaps, contrast failures and accessibility findings. Generated from audit.md.
Source: https://www.vv-it.com/webordering/static/ (UAT build v0.50.3, blue theme), 13 pages crawled at 1440px and 390px on 2026-10-07 with an authenticated UAT account (read-only). All findings come from computed styles, rendered text and the site's public CSS/JS. Severity: High = blocks users or fails WCAG AA; Med = inconsistency that costs rework; Low = tidy-up.
Summary
| Area | Finding | Sev |
|---|---|---|
| Contrast | 16 distinct text/background pairs fall below WCAG AA (worst 1.26:1) | High |
| Focus | No author-defined :focus / :focus-visible styles on any page; the only rule (usermenu.js) removes the outline. Controls rely on the browser default ring |
High |
| Forms | 720 text inputs/selects have no programmatic label (placeholder only); 600 of them are the Rx grid on rx-lookup |
High |
| Keyboard | 55 onclick handlers on non-interactive elements (53 table cells on user-management, 2 on tracking) are unreachable by keyboard |
High |
| Responsive | Horizontal overflow at 390px on 6 of 13 pages (worst: branch-admin +720px); only two media queries exist |
High |
| Color | 75 distinct opaque colors collapse to 42 tokens; two parallel neutral palettes (theme.css vs hard-coded Tailwind-style grays) | Med |
| Semantics | --green is bound to the theme accent, so "ok"/success text renders blue in the UAT theme; --teal variables hold blue |
Med |
| Components | 33 button variants, 6 button class families, 8 input variants, 4 modal implementations + 1 drawer, 4 error-message treatments | Med |
| Type / space | 14 font sizes (half-pixel steps), 6 font stacks, 33% of spacing values off a 4px-based scale | Med |
| Structure | No <main>/<header>/<footer> landmarks, almost no <h1>, th without scope, modals without role="dialog" (except login) |
Med |
| Errors | 404 returns bare JSON; API 500 on order-drafts/{id}/delivery-preview and 403 on admin/tog/connection seen during crawl |
Low |
1. Near-duplicate values
Colors (merged into tokens)
| Token | Canonical | Near-duplicates observed (uses) |
|---|---|---|
color.text.default (color) |
#22292f |
#1f2937 (239) |
color.text.muted (color) |
#6b7280 |
#6b7680 (398), #64748b (8) |
color.primary.tint-1 (color) |
#d6e4f7 |
#cfe3f5 (5) |
color.success.text (color) |
#0f6b34 |
#166534 (4) |
color.bg.control (bg) |
#e3e8ef |
#e5e7eb (5) |
color.bg.subtle (bg) |
#f7fafd |
#f9fafb (60), #f8fafc (24), #f7f9fc (12), #fafbfd (3), #f6fbf8 (2) |
color.bg.sunken (bg) |
#eef1f6 |
#eef3f8 (11), #efefef (4) |
color.bg.page (bg) |
#f3f6fa |
#f1f5f9 (8), #f3f6f9 (3), #f3fbf6 (2), #f0fdf4 (2), #f3f5f7 (1) |
color.success.bg (bg) |
#e2f4e8 |
#dcfce7 (4) |
color.primary.tint-4 (bg) |
#e1ebf8 |
#e0e7ff (6), #dfe9f6 (2) |
color.border.subtle (borderColor) |
#e0e7f0 |
#e5e7eb (2445) |
Two neutral palettes. Theme variables (--wo-*, e.g. text #22292f, muted #6b7680, border #e0e7f0) sit beside hard-coded Tailwind-style grays (#111827 ×1401, #6b7280 ×936, #374151, #e5e7eb ×2445). Pick one. Five reds for errors: #c0392b, #842029, #a03026, #b91c1c, #b3261e. Four greens for success text: #0f6b34, #166534, #15803d, #0a7d32.
Typography
- Font sizes (14): 10.0px×3, 11.0px×21, 11.5px×369, 12.0px×145, 12.5px×2459, 13.0px×2103, 13.3333px×1, 13.5px×299, 14.0px×63, 14.5px×15, 15.0px×15, 16.0px×43, 17.0px×7, 20.0px×11. Half-pixel sizes (11.5, 12.5, 13.5, 14.5) account for ~3,100 text elements;
12.5pxalone is ×2459. - Font stacks (6):
"Noto Sans Thai","Segoe UI",Tahoma,sans-serif(×2495),system-ui,…(×2406),"Segoe UI",Tahoma,sans-serif(×343),monospace(×290),Arial(×19, unstyled form controls),ui-monospace,…(×1). No@font-faceor web-font link is loaded on any page, so "Noto Sans Thai" only applies if installed locally. - Line-height: almost always
normal; explicit values between ~1.35 and 1.7 appear ad hoc (Thai text benefits from an explicit value). - Weights: 400/600/700 plus one-offs 500, 800, 900.
Radius, motion, breakpoints
- Radius (9 values): 6, 7, 8, 9, 10, 12, 14, 99 and 999px. Inputs alternate between 6/7/8px;
99pxand999pxare both "pill". - Motion:
transition: all .15s easeon 53 elements (animates every property; prefer named properties) andtransform .18son the drawer. Noprefers-reduced-motionhandling. - Breakpoints:
max-width: 700pxandmax-width: 800pxare both used once — near-duplicates, and the only responsive rules on the site. - Shadows (4): card
0 2px 10px, sm0 1px 3px, drawer, inset tab; modal shadow0 12px 40px rgba(0,0,0,.25)is defined in CSS but not in use at crawl time.
2. One-off and off-scale spacing
33.0% of spacing declarations (≤40px) do not land on the proposed scale (2/4/6/8/12/16/20/24/36). Most common off-scale values:
| Value | Uses | Snaps to |
|---|---|---|
| 9px | 3092 | 8px |
| 5px | 2926 | 6px |
| 7px | 2068 | 8px |
| 11px | 478 | 12px |
| 10px | 225 | 12px |
| 18px | 160 | 20px |
| 3px | 159 | 4px |
| 14px | 103 | 16px |
Table cells use padding: 6px 9px pervasively; cards use 18px padding where the rest of the UI uses 16/20. Other one-offs: margin: -2px 0 10px on #groupTabs, modal padding: 90px 16px for vertical offset, min-width: 34px on .qty.
3. Component inconsistencies
| Component | Observation |
|---|---|
| Button | 33 computed variants. Class families btn-teal, btn-primary, btn-gray, btn-secondary, btn-ghost, btn-danger overlap (btn-gray ≈ btn-secondary ≈ btn-ghost; btn-teal ≈ btn-primary). btn-teal is blue in the UAT theme. Same variant renders at 30px (12.5px text, 5/11 padding) on some pages and 33px (13px, 6/12) on others. |
| Input | Radius 6/7/8px, padding 4/6 → 11/12, heights 26–41px; some inputs are unstyled UA controls (0 radius, 13.33px, Arial). |
| Modal | Four implementations + a drawer (section 8): .modal-backdrop/.modal-card (POS gate, settings confirm), .ov (login ×2), .um-overlay (change password), .modal-bg/.modal (CS ticket), .drawer (tracking). z-index 2000 on four, 50 on the rest; widths 420/480/520px; scrim rgba(0,0,0,.45) ×3 and .5 ×2; shadow .25 vs .3. |
| Banner/alert | .banner is defined at least 3 ways (radius 8 vs 10, padding 10/12 vs 12/14, font 13.5 vs 14); .err appears with 4 different reds/backgrounds; .status.ok uses #15803d on #f0fdf4 while .banner-done uses #0f6b34 on #e2f4e8. |
| Badge/pill | 22 computed variants across .pill, .badge, .chip, .togtag, .cnt, .status; radius 99/999px, font 11/11.5/12/13px. |
| Tabs | .btn-tab, .btn-sub, .tab are three tab idioms; active state differs (fill vs inset shadow vs pill). |
| Links | 47 text elements use the browser-default link blue #0000ee; others use --wo-primary-dark. |
| Table | 7 variants; th fill, border and padding differ per page; none use scope, <caption>, or a responsive pattern. |
| Page chrome | Top bar differs: .topbar vs .top; phase chip restyled per bar. No footer anywhere (version badge is fixed bottom-right). |
| Page titles | VV Web Ordering — X on 9 pages, X — VV Web Ordering on 4 (orders, tracking, audit, review). |
4. State coverage (hover · focus · disabled · error)
- Hover: only 2 of 33 button variants change on hover (the user-menu chip and the sortable table-header buttons).
.btnhas no hover or active feedback. Only 15:hoverrules exist across all page CSS andusermenu.js. - Focus: zero author
:focus/:focus-visiblerules in page CSS. Keyboard focus relies on the UA ring (outline: auto), whose visibility varies with the background (e.g. on the blue top bar and the blue active tab).usermenu.jssets.um-mbody input:focus{outline:none; border-color: …}— a 1px border-color change, below WCAG 2.4.11 expectations. - Disabled: one rule,
.btn:disabled{opacity:.5}. Disabled inputs/selects have no styling;opacity:.5also drops white-on-#1d5fb4text to ~2.3:1 (acceptable only because disabled controls are exempt). - Error: no
:invalid/aria-invalidstyles. Errors are shown in free-standing.errblocks with no association to the input (aria-describedby) and no live-region (role="alert"appears on 1 page). - Active/pressed: none defined.
5. Contrast failures (WCAG 2.2 AA: 4.5:1 text, 3:1 large)
| Ratio | Foreground on background | Size | Example (page) |
|---|---|---|---|
| 1.26 | #6b7680 on #3b82f6 |
12.5px | “(คลังมองเด้ · ตัดประกอบที่ VVD)” (settings) |
| 2.01 | #b0b8bd on #ffffff |
14.5px | “— ไม่ได้ระบุ —” (review) |
| 3.68 | #ffffff on #3b82f6 |
11px | “ใช้งานอยู่” (settings) |
| 3.68 | #ffffff on #3b82f6 |
13px | “1.” (settings) |
| 4.05 | #6b7680 on #e2f4e8 |
12.5px | “⚠️ เอกสารยังไม่ถูก Release — คลังย” (review) |
| 4.28 | #ffffff on #467cc2 |
11.5px | “Phase 2.1” (rx-lookup, stock) |
| 4.28 | #ffffff on #467cc2 |
13px | “Phase 2.1” (branch-admin, settings, tog-admin, user-management) |
| 4.34 | #64748b on #f1f5f9 |
11.5px | “ปิด” (branch-admin, tog-admin) |
| 4.4 | #6b7680 on #f3fbf6 |
13px | “Server” (settings) |
| 4.4 | #6b7680 on #f3fbf6 |
12.5px | “บิลที่มีกรอบ FS → · หน้าเปิดออเดอร” (settings) |
| 4.4 | #6b7680 on #f7f9fc |
12px | “สาขา pilot 2.1” (branch-admin, tog-admin) |
| 4.4 | #6b7680 on #f7f9fc |
12.5px | “รหัสนี้เป็น ใช้เข้าคอมพิวเตอร์บริษ” (login) |
| 4.42 | #6b7280 on #f3f5f7 |
12.5px | “(footer credit line)” (signoff) |
| 4.43 | #6b7680 on #f6fbf8 |
12.5px | “กำลังตรวจสต็อก BC…” (review) |
| 4.48 | #6b7680 on #fafbfd |
13px | “Server” (settings) |
| 4.48 | #6b7680 on #fafbfd |
12.5px | “บิลที่มีกรอบ FS → · หน้าเปิดออเดอร” (settings) |
The systemic ones: #6b7680 muted text on tinted surfaces (4.05–4.48:1; passes only on pure white), white on #3b82f6 (3.68:1), and the translucent phase chip (4.28:1). Fix by darkening text.muted to #5b6670 (5.1–5.9:1 on white and the tinted surfaces) and using primary.default (#1d5fb4, 6.3:1 with white) for filled badges instead of accent.bright.
6. Accessibility & structure
| Check | Result |
|---|---|
| Targets under 24×24px | 109 elements, mostly inline .muted links and .back links (exempt as inline text); real offenders: .btn-ghost (17px tall ×8), Rx grid inputs (26px) |
| Unlabeled controls | index: 1, orders: 2, tracking: 3, stock: 3, rx-lookup: 602, audit: 5, settings: 1, user-management: 53, branch-admin: 29, tog-admin: 19, login: 2 |
| Landmarks | <main>: 0/13 · <header>: 0/13 · <nav>: 1/13 · <footer>: 0/13 · skip link: none |
| Headings | <h1> present on 2 pages (signoff ×2, login ×1); other pages start at <h2> |
| Tables | 167 th cells without scope |
| Mouse-only handlers | onclick on td/tr/div/span: user-management 53, tracking 2 |
| Dialogs | role="dialog"/aria-modal: login only (2). Focus trapping / return-focus in the others was not verified (modals were not opened) |
| Language | lang="th" on all pages (good); English UI strings (Username, Password, Settings, Audit log) are not marked lang="en" |
| Mobile overflow @390px | orders +304px, stock +143px, audit +284px, review +113px, branch-admin +720px, tog-admin +345px |
| Zoom / viewport | <meta name="viewport"> present on all pages (good) |
8. Modals and error states (Stage 6 — triggered in UAT)
Modals were opened (never confirmed) and error states were provoked with bogus input. Writes were limited to two rejected sign-in attempts (POST /api/auth/login, POST /api/pos/refresh with an invented username); the password-change, CS-ticket and settings-save actions were not submitted — their validation errors were captured client-side or the dialog was cancelled.
Modal comparison
| Modal (page) | z-index | Scrim | Width | Radius | Shadow | Title | Primary button | role=dialog / aria-modal |
|---|---|---|---|---|---|---|---|---|
| First-change password (login) | 2000 | 0, 0, 0, 0.5 |
420px | 12px | α0.3) |
16px / 700 | #1d5fb4 @ 13.5px |
dialog / true |
| POS gate (bills) | 2000 | 0, 0, 0, 0.45 |
420px | 12px | α0.25) |
16px / 700 | #1d5fb4 @ 13px |
dialog / true |
| Change password (user menu) | 2000 | 0, 0, 0, 0.5 |
420px | 12px | α0.3) |
16px / 700 | #2563eb @ 13.5px |
dialog / true |
| CS ticket (review) | 50 | 0, 0, 0, 0.45 |
520px | 12px | α0.25) |
17px / 700 | #2f6fd6 @ 14.5px |
— / — |
| Settings confirm (settings) | 2000 | 0, 0, 0, 0.45 |
480px | 12px | α0.25) |
16px / 700 | #1d5fb4 @ 14px |
dialog / true |
| Side drawer (tracking) | 50 | none (no scrim) | 520px | 0 | -4px 0 18px / 0.15 |
16px / 700 | n/a | — / — |
- Four different “primary” blues appear across these dialogs:
#1d5fb4,#2563eb(change password),#2f6fd6(CS ticket, with 9px radius and 11×20 padding) and#154a8f(login submit). Button size also drifts (13px → 14.5px; padding 6×12 → 11×20). - Dismissal differs: only the settings dialog handles Esc (and has ✕ + backdrop click); change-password deliberately closes by button only (stated in
usermenu.js); the login, POS-gate and CS-ticket dialogs have no Esc handler in their source. - Accessibility: the CS-ticket dialog has no
role="dialog"/aria-modal/ label; the tracking drawer has none either. On open, focus moves to the first field (or the confirm button in settings) in all five, but no focus trap or focus-return appears in the source that was read; not tested interactively.
Error and validation treatments
| Where | Trigger | Treatment | Colors | Announced to AT? |
|---|---|---|---|---|
| Login | wrong credentials (server) | boxed message, 13px, radius 8 | #c0392b on #fdecea, border #f3c1bc |
no (role/aria-live absent) |
| First-change password | empty / <8 chars (client) | same boxed message as login | same | no |
| Change password (menu) | empty / <8 / mismatch (client) | boxed message, 13.5px, padding 9×12 | #b91c1c on #fef2f2 |
no |
| POS gate | rejected credentials (server) | plain red text, no box | #c0392b |
no |
| CS ticket | send failure | plain red text (.err), no box |
#b3261e |
no |
| Stock check | unknown SKU | inline bold red line with ❌, raw JSON shown (({})) |
#c0392b |
no |
All error text passes AA on its own background (4.76–6.54:1), but there are four visual treatments and five reds (#c0392b, #b91c1c, #b3261e, #842029, #a03026). None is tied to its field (aria-describedby / aria-invalid) or announced (role="alert"). The login message exposes the lockout rule (“3 more attempts before 30-minute lock”), which is useful but is colored the same as an error. The stock error prints a raw ({}) payload.
Empty and no-result states
| Page | Behavior |
|---|---|
Bills (index) |
Table renders headers only; the only hint is small grey text above (“0 บิล”). No empty-row message. |
| Orders | One muted row “— ไม่พบออเดอร์ที่ตรงกับ "…" —” (dash-wrapped). The filter form uses unstyled browser controls (no radius, default buttons) unlike the bills page. |
| Audit log | One muted row “— ไม่มีรายการ —”; filter inputs are styled (radius 8) but the submit button is #1f4e79, a fifth blue. |
| Rx lookup | Plain muted line outside the table: “ไม่พบลูกค้าที่ตรงกับคำค้น” (no dashes, no row). |
| User management | Table row “ไม่พบ User ที่ตรงกับคำค้น” (no dashes). |
| Tracking drawer | Loads on row click (read-only GET); shows a muted “กำลังโหลด…” then the history. |
Five pages, four different empty-state treatments (none on bills, dash-wrapped row, plain row, outside-the-table line).
7. Recommended order of work
- Add one global
:focus-visiblering (2px solid primary.strong, offset 2px) and stop removing outlines — fixes the biggest a11y gap in one rule. - Label every field (
<label for>oraria-label), starting with the Rx grid and user management; turn clickable cells into buttons/links. - Adopt
tokens.cssas the single source: replace the 5 reds / 4 greens / two grays, and rename--teal/--greento semantic names that do not change with theme. - Collapse button families to
primary | secondary | danger | ghost×md | sm, and one modal + one banner implementation. - Introduce 700/480px (or a single 720px) breakpoints and a responsive table pattern; fix the 6 overflowing pages.
- Darken
text.muted, fix white-on-#3b82f6badges and the phase chip. - Add landmarks,
<h1>,scopeonth,role="dialog", and a styled 404. - Standardise one modal (see Components), one error message (
role="alert",aria-describedby) and one empty state.
Method & limits
Read-only: pages were loaded and screenshotted; hover/focus/active were forced via DevTools (no clicks), and non-GET API calls were blocked. State "disabled" was toggled in-page without submitting. Stage 6 provoked error states with bogus input (two rejected sign-ins) and opened modals without confirming them; password-change, CS-ticket send, settings save, order-open/submit/cancel and admin create/edit actions were not exercised, so their server-side success/failure paths are unverified. Findings reflect the UAT build and the blue theme; the green production theme was not crawled.