# WCAG 2.1 AA Accessibility Audit Report — DocuSeal
**Date:** 2026-02-25
**Standard:** WCAG 2.1 Level AA
**Scope:** ERB views, Vue components, custom JS elements, color contrast
**Status:** FAIL — multiple violations identified
---
## Executive Summary
Four specialized audit agents reviewed the full codebase across four domains. A total of **~80 issues** were identified: 20 critical, ~30 major, ~30 minor. The most impactful failures cluster around six themes:
1. Viewport zoom disabled (users with low vision cannot zoom)
2. Focus indicators removed on many inputs (keyboard users cannot see focus position)
3. Modal focus not managed (keyboard users get trapped or lose focus)
4. `alert()` / `prompt()` used for errors (screen readers cannot process these)
5. Form controls lack proper labels (screen readers cannot describe fields)
6. Validation errors not announced via live regions
The recent PDF/Text tab switcher implementation is noted as **correctly implemented** (proper ARIA tablist/tab/tabpanel, roving tabindex, keyboard navigation). The sr-only page text pattern is also correctly implemented.
---
## Priority 1 — Critical (Fix Immediately)
### C1. Viewport Zoom Disabled
**WCAG:** 1.4.4 Resize Text (AA)
**File:** `app/views/layouts/application.html.erb:8`
**Issue:** `maximum-scale=1.0, user-scalable=no` prevents users from zooming to 200%.
**Fix:** Change to ``
---
### C2. Focus Indicators Removed Without Alternative
**WCAG:** 2.4.7 Focus Visible (AA)
**Files:**
- `app/views/templates/_file_form.html.erb:11` — `outline-none focus:ring-0`
- `app/views/templates_preferences/_recipients.html.erb:15` — same
- `app/views/templates_clone/_form.html.erb:17` — same
- `app/javascript/submission_form/phone_step.vue:105` — `!outline-none`
- `app/javascript/template_builder/font_modal.vue:172` — `outline-none`
- `app/javascript/template_builder/area.vue:67` — `outline-none` on contenteditable
- `app/javascript/template_builder/document.vue:9-30` — `focus:outline-none` on tab buttons (no alternative)
**Fix:** Replace `outline-none focus:ring-0` with `focus:ring-2 focus:ring-base-content focus:ring-offset-1` on each.
---
### C3. Modal Focus Management Absent
**WCAG:** 2.4.3 Focus Order (A), 2.1.2 No Keyboard Trap (A)
**File:** `app/javascript/elements/turbo_modal.js` (entire file)
**Issues:**
- No focus trap within modal (Tab escapes the modal)
- No focus moved into modal on open
- No focus restoration to trigger element on close
- No `role="dialog"` or `aria-modal="true"`
**Fix:** On open, store trigger reference, move focus to first focusable element, trap Tab/Shift+Tab; on close, restore focus to trigger. Add `role="dialog" aria-modal="true" aria-labelledby=""`.
---
### C4. alert() / prompt() Used for All Errors
**WCAG:** 3.3.1 Error Identification (A), 4.1.3 Status Messages (AA)
**Files:**
- `app/javascript/elements/fetch_form.js:24`
- `app/javascript/elements/download_button.js:48`
- `app/javascript/elements/clipboard_copy.js:20`
- `app/javascript/elements/prompt_password.js:7`
- `app/javascript/submission_form/signature_step.vue:765-773`
**Fix:** Replace every `alert()` / `prompt()` with ARIA live region announcements (`role="alert"` or `aria-live="assertive"`). For `prompt_password.js`, replace browser prompt with a custom `role="dialog"` modal containing a labelled input.
---
### C5. Form Controls Lack Associated Labels
**WCAG:** 1.3.1 Info and Relationships (A), 4.1.2 Name Role Value (A)
**Files:**
- `app/javascript/submission_form/signature_step.vue:209-216` — signature text input has no `