Skip to content

Accessibility Features

Anthony Baggett edited this page Nov 21, 2025 · 1 revision

Accessibility Features

VAST is built with accessibility in mind, following WCAG 2.1 AA guidelines to ensure the theme is usable by everyone.


Semantic HTML

Proper use of HTML5 semantic elements ensures assistive technologies can understand page structure.

Landmark Elements

  • <header> - Site header with navigation
  • <nav> - Navigation menus (main menu, footer menu, pagination)
  • <main> - Primary page content
  • <footer> - Site footer with secondary links
  • <aside> - Sidebar content (filters, related content)
  • <article> - Blog posts and standalone content
  • <section> - Thematic grouping of content

Heading Hierarchy

Proper heading structure (h1, h2, h3) throughout all templates:

  • One h1 per page - Page title or main heading
  • Logical h2-h6 nesting - No skipped heading levels
  • Descriptive headings - Clearly describe section content

Lists and Navigation

  • <ul>/<ol> for all list-based content
  • <nav> wrapper for all navigation menus
  • Proper link markup with descriptive text (no "click here")

ARIA Support

ARIA Labels

Interactive elements have descriptive labels:

<button aria-label="Add {{ product.title }} to cart">
  {% render 'icon-cart' %}
</button>

<a href="/cart" aria-label="Shopping cart with {{ cart.item_count }} items">
  {% render 'icon-cart' %}
  <span>{{ cart.item_count }}</span>
</a>

ARIA Roles

Dynamic content has appropriate roles:

  • role="listbox" - Search results dropdown
  • role="navigation" - Menu systems
  • role="dialog" - Modal dialogs
  • role="alert" - Error messages and notifications
  • role="status" - Loading states and status updates

ARIA States

Dynamic elements communicate their state:

  • aria-expanded="true|false" - Collapsible content (FAQ, mobile menu)
  • aria-hidden="true|false" - Visibility state (modals, drawers)
  • aria-current="page" - Current page in navigation
  • aria-live="polite" - Dynamic cart updates
  • aria-busy="true" - Loading states during async operations

Example: Collapsible FAQ

<button
  aria-expanded="false"
  aria-controls="faq-{{ forloop.index }}"
  @click="open = !open">
  {{ block.settings.question }}
</button>

<div
  id="faq-{{ forloop.index }}"
  x-show="open"
  x-collapse
  role="region"
  aria-labelledby="faq-button-{{ forloop.index }}">
  {{ block.settings.answer }}
</div>

Keyboard Navigation

Full keyboard support for all interactive elements.

Skip to Content Link

Skip link visible on focus allows keyboard users to bypass navigation:

<a href="#main-content" class="skip-to-content">
  Skip to content
</a>

<main id="main-content" tabindex="-1">
  <!-- Page content -->
</main>

Styled to appear only when focused:

.skip-to-content {
  position: absolute;
  top: -100px;
  left: 0;
}

.skip-to-content:focus {
  top: 0;
}

Modal and Dialog Support

Full keyboard support for modals:

  • Tab key - Navigate between focusable elements
  • Shift + Tab - Navigate backwards
  • Escape key - Close modal
  • Focus trapping - Tab cycles within modal (via @alpinejs/focus)
  • Focus restoration - Returns focus to trigger element on close

Implemented with Alpine.js Focus plugin:

<div
  x-show="open"
  x-trap.noscroll="open"
  role="dialog"
  aria-modal="true"
  @keydown.escape="open = false">
  <!-- Modal content -->
</div>

Interactive Element Support

All interactive elements are keyboard accessible:

  • Buttons - Activated with Space or Enter
  • Links - Activated with Enter
  • Form controls - Standard keyboard navigation
  • Custom controls - Proper tabindex and keyboard event handlers

Visual Accessibility

Color Contrast

All text meets WCAG AA contrast requirements:

Brand Colors (WCAG Compliant):

  • blue-600 on white: 4.76:1 (AA ✓)
  • blue-700 on white: 6.1:1 (AAA ✓)
  • White on blue-600: 4.76:1 (AA ✓)
  • White on blue-700: 6.1:1 (AAA ✓)

Button States:

  • Default: bg-blue-600 text-white (4.76:1)
  • Hover: hover:bg-blue-700 (6.1:1, provides visible feedback)
  • Focus: focus:ring-2 focus:ring-blue-400 (visible focus ring)

Focus Indicators

Visible focus indicators on all interactive elements:

button:focus-visible,
a:focus-visible {
  outline: 2px solid theme('colors.blue.500');
  outline-offset: 2px;
}

Custom focus ring utilities:

class="focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2"

Form Validation

Accessible form validation with error messages:

<label for="email">Email Address</label>
<input
  type="email"
  id="email"
  name="email"
  required
  aria-describedby="email-error"
  aria-invalid="{{ form.errors contains 'email' }}">

{% if form.errors contains 'email' %}
  <span id="email-error" role="alert" class="text-red-600">
    {{ form.errors.translated_fields['email'] | capitalize }}
    {{ form.errors.messages['email'] }}
  </span>
{% endif %}

Loading States

Visual feedback for async operations:

<button
  type="submit"
  :disabled="loading"
  :aria-busy="loading">
  <span x-show="!loading">Add to Cart</span>
  <span x-show="loading">
    {% render 'icon-spinner' %}
    <span class="sr-only">Adding to cart...</span>
  </span>
</button>

Screen Reader Support

Visually Hidden Content

Content visible to screen readers but hidden visually:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Usage:

<button>
  {% render 'icon-cart' %}
  <span class="sr-only">Shopping cart</span>
</button>

Live Regions

Dynamic content updates announced to screen readers:

<div aria-live="polite" aria-atomic="true">
  <span class="sr-only">
    Cart updated: {{ cart.item_count }} items, total {{ cart.total_price | money }}
  </span>
</div>

Image Alt Text

All images have descriptive alt text:

<img
  src="{{ product.featured_image | img_url: '800x' }}"
  alt="{{ product.featured_image.alt | default: product.title }}"
  loading="lazy">

Decorative images use empty alt:

<img src="{{ 'decorative-pattern.svg' | asset_url }}" alt="" role="presentation">

Form Accessibility

Labels and Inputs

All form inputs have associated labels:

<label for="customer-email">Email Address</label>
<input
  type="email"
  id="customer-email"
  name="customer[email]"
  autocomplete="email"
  required>

Required Field Indicators

Required fields clearly marked:

<label for="name">
  Full Name
  <abbr title="required" aria-label="required">*</abbr>
</label>
<input type="text" id="name" name="name" required>

Autocomplete Attributes

Proper autocomplete values for common fields:

<input type="email" autocomplete="email">
<input type="text" autocomplete="given-name">
<input type="text" autocomplete="family-name">
<input type="tel" autocomplete="tel">
<input type="text" autocomplete="address-line1">

Testing Your Site's Accessibility

Automated Testing Tools

  • axe DevTools - Browser extension for accessibility testing
  • WAVE - Web accessibility evaluation tool
  • Lighthouse - Built into Chrome DevTools

Manual Testing

Keyboard Navigation:

  1. Unplug your mouse
  2. Navigate site using only Tab, Shift+Tab, Enter, Space, Escape
  3. Ensure all interactive elements are reachable and usable

Screen Reader Testing:

  • macOS: VoiceOver (Cmd + F5)
  • Windows: NVDA (free) or JAWS
  • Mobile: TalkBack (Android) or VoiceOver (iOS)

Color Contrast:

  • Use browser DevTools color picker
  • Check contrast ratios meet WCAG AA (4.5:1 for normal text)

Next Steps


← Back to Home