Skip to content

Last updated:

WCAG Guidelines

CPR should target WCAG 2.1 Level AA compliance. This document covers the most relevant guidelines for the application, with specific attention to CPR's design system (primary color #117ea7, Tailwind CSS utility classes, and clinical form patterns).

Color Contrast

CPR Primary Color: #117ea7

The primary brand color #117ea7 (a teal/blue) is used throughout the application for buttons, modal headers, and focus rings.

Contrast ratios with common backgrounds:

ForegroundBackgroundRatioWCAG AA (Normal Text)WCAG AA (Large Text)
#117ea7White #ffffff~4.0:1Fails (needs 4.5:1)Passes (needs 3:1)
White #ffffff#117ea7~4.0:1FailsPasses
#117ea7#f9fafb (gray-50)~3.9:1FailsPasses

Recommendations:

  • Use #117ea7 on white only for large text (18px+ or 14px+ bold), which is how CPR uses it for button text and modal headers with font-semibold or font-bold.
  • For small body text, consider a darker variant (e.g., #0d5f80) that achieves 7:1+ contrast.
  • The focus ring focus:ring-[#117ea7]/10 is purely decorative (reinforced by the border change) and does not carry meaning alone, so its lower opacity is acceptable.

Other Color Considerations

  • Error red (bg-red-600, text-red-500) -- Tailwind's red-600 on white has a ratio of ~4.6:1 (passes AA). red-500 is ~4.0:1 (borderline -- use for large text or alongside icons).
  • Success green (bg-green-600) -- Tailwind's green-600 on white has ~4.5:1 ratio (passes AA).
  • Gray text (text-gray-600) -- Tailwind's gray-600 on white has ~5.7:1 ratio (passes AA).
  • Disabled state (disabled:opacity-50) -- Disabled elements are exempt from contrast requirements per WCAG, but should still be visually distinguishable.

Never Rely on Color Alone

The UiSnackBar uses color to distinguish message types, but also includes text content. Ensure all status indicators include a text or icon supplement:

vue
<!-- Good -- color + text -->
<UiBadge variant="success">ACTIVE</UiBadge>
<UiBadge variant="warning">PENDING</UiBadge>

<!-- Avoid -- color only -->
<div :class="{ 'bg-green-500': active, 'bg-red-500': !active }" />

Form Field Requirements

Labels

Every form input must have a visible label. The UiInput component handles this automatically:

vue
<!-- UiInput generates a proper label-input association -->
<UiInput
  v-model="patient.last_name"
  label="Last Name"
  required
/>

This renders:

html
<label for="input-abc1234" class="text-xs font-bold text-gray-700 ...">
  Last Name
  <span class="text-red-500">*</span>
</label>
<input id="input-abc1234" type="text" ... />

Required Field Indicators

CPR marks required fields with a red asterisk (*). This is a common convention, but screen reader users need additional context. Enhance with aria-required:

vue
<!-- Recommended enhancement to UiInput -->
<input
  :id="id"
  :aria-required="required"
  :aria-invalid="!!error"
  :aria-describedby="error ? `${id}-error` : undefined"
  ...
/>
<div v-if="error" :id="`${id}-error`" class="mt-1 text-xs text-red-500">
  {{ error }}
</div>

Error Messaging

Validation errors should be:

  1. Visible -- Displayed below the relevant field (CPR does this).
  2. Descriptive -- Include the field name and what is wrong: "Patient Name is required" not just "Required".
  3. Programmatically associated -- Connected to the input via aria-describedby.
  4. Announced -- Use aria-live="polite" on the error container or aria-invalid on the input.

CPR's validation rules already produce descriptive messages:

ts
// "Patient Name is required" -- clear and descriptive
required('Patient Name');

// "Amount must be at least 1" -- includes the constraint
minValue('Amount', 1);

Keyboard Navigation

Focus Order

The tab order should follow the visual layout of the page. CPR uses standard HTML flow, which naturally creates a logical tab order. Avoid using tabindex values greater than 0, which override the natural order.

Interactive Elements

All interactive elements must be keyboard-accessible:

ElementExpected Keyboard Behavior
UiButtonEnter or Space activates
UiInputTab focuses, typing enters text
UiModal close buttonEscape closes the modal
UiTable clickable rowsShould support Enter to activate
UiDropdownArrow keys to navigate, Enter to select
UiSearchSelectType to filter, arrow keys to navigate
UiPaginationTab between page buttons, Enter to navigate

Focus Visibility

CPR's UiInput has clear focus styles:

css
focus:bg-white focus:outline-none focus:border-[#117ea7] focus:ring-4 focus:ring-[#117ea7]/10

Ensure all interactive elements have visible focus indicators. Never use outline: none without providing an alternative focus style.

Screen Reader Considerations

Headings

Use a logical heading hierarchy on each page:

vue
<template>
  <h1>Patient Records</h1>           <!-- Page title -->
  <h2>Search and Filter</h2>         <!-- Section -->
  <h2>Patient List</h2>              <!-- Section -->
  <h3>Juan Dela Cruz</h3>            <!-- Patient name in detail view -->
</template>

Tables

The UiTable component uses proper <table>, <thead>, <th>, <tbody>, and <td> elements. Enhance with scope attributes:

html
<th scope="col">Patient Name</th>
<th scope="col">Status</th>

Landmarks

Use ARIA landmarks or semantic elements to structure the page:

vue
<template>
  <nav aria-label="Main navigation">...</nav>
  <main>
    <h1>Queue Management</h1>
    <!-- page content -->
  </main>
</template>

Testing for WCAG Compliance

Automated Tools

  • axe DevTools -- Browser extension that scans for WCAG violations.
  • Lighthouse -- Built into Chrome DevTools, includes accessibility audit.
  • vue-axe -- Vue plugin that reports accessibility issues in the browser console during development.

Manual Testing

  • Navigate the entire application using only the keyboard (Tab, Enter, Escape, Arrow keys).
  • Use a screen reader (VoiceOver on macOS, NVDA on Windows) to verify that content is announced correctly.
  • Test with browser zoom at 200% to ensure layout does not break.
  • Verify that all form fields are operable without a mouse.

CPR - Clinical Patient Records