Files
thrillwiki_django_no_react/docs/accessibility/component-patterns.md
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

8.1 KiB

Accessible Component Patterns

This document provides code examples for creating accessible components in ThrillWiki. Follow these patterns to ensure WCAG 2.1 AA compliance.

Button

Standard Button

{% include 'components/ui/button.html' with
    text='Save Changes'
    variant='primary'
    type='submit'
%}

Icon Button (REQUIRED: aria_label)

{% include 'components/ui/button.html' with
    icon=close_svg
    size='icon'
    aria_label='Close dialog'
    variant='ghost'
%}

Important: Icon-only buttons MUST include aria_label for screen reader accessibility.

Disabled Button

{% include 'components/ui/button.html' with
    text='Submit'
    disabled=True
%}

Button with HTMX

{% include 'components/ui/button.html' with
    text='Load More'
    hx_get='/api/items?page=2'
    hx_target='#item-list'
    hx_swap='beforeend'
%}

Form Field

Standard Field

{% include 'forms/partials/form_field.html' with
    field=form.email
%}

The form_field.html component automatically handles:

  • Label association via for attribute
  • Error message display with proper ARIA
  • Required field indication
  • Help text with aria-describedby

Field with Help Text

{% include 'forms/partials/form_field.html' with
    field=form.password
    help_text='Must be at least 8 characters'
%}

Field with HTMX Validation

{% include 'forms/partials/form_field.html' with
    field=form.username
    hx_validate=True
    hx_validate_url='/api/validate-username/'
%}

Complete Form Example

<form method="post" role="form" aria-label="User registration form">
    {% csrf_token %}

    <fieldset>
        <legend class="sr-only">Account Information</legend>
        {% include 'forms/partials/form_field.html' with field=form.username %}
        {% include 'forms/partials/form_field.html' with field=form.email %}
    </fieldset>

    <fieldset>
        <legend class="sr-only">Security</legend>
        {% include 'forms/partials/form_field.html' with field=form.password1 %}
        {% include 'forms/partials/form_field.html' with field=form.password2 %}
    </fieldset>

    {% include 'components/ui/button.html' with
        text='Create Account'
        type='submit'
        variant='primary'
    %}
</form>

Modal

Basic Modal

{% extends 'components/modals/modal_base.html' %}

{% block modal_body %}
    <p>Are you sure you want to delete this item?</p>
{% endblock %}

Modal with Actions

{% extends 'components/modals/modal_base.html' %}

{% block modal_body %}
    <p>This action cannot be undone.</p>
{% endblock %}

{% block modal_footer %}
    <button @click="{{ show_var }} = false" type="button">
        Cancel
    </button>
    {% include 'components/ui/button.html' with
        text='Delete'
        variant='destructive'
        x_on_click='confirmDelete()'
    %}
{% endblock %}

The modal_inner.html component automatically provides:

  • role="dialog" and aria-modal="true"
  • aria-labelledby pointing to title
  • aria-describedby pointing to body (and subtitle if present)
  • Focus trap with Tab/Shift+Tab cycling
  • Home/End key support for first/last focusable element
  • Escape key to close (configurable)
  • Auto-focus on first focusable element

Navigation Menu

Dropdown Menu

<div x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
    <button
        @click="open = !open"
        aria-haspopup="true"
        :aria-expanded="open.toString()"
        aria-label="User menu">
        <span>Menu</span>
        <i class="fas fa-chevron-down" aria-hidden="true"></i>
    </button>

    <div
        x-show="open"
        x-transition
        role="menu"
        aria-label="User account options"
        class="dropdown-menu">

        <a role="menuitem" href="/profile" class="menu-item">Profile</a>
        <a role="menuitem" href="/settings" class="menu-item">Settings</a>
        <div role="separator" class="border-t"></div>
        <button role="menuitem" type="submit" class="menu-item">Logout</button>
    </div>
</div>

Accessible Search with Results

<div role="search">
    <label for="search-input" class="sr-only">Search parks and rides</label>
    <input
        id="search-input"
        type="search"
        placeholder="Search..."
        hx-get="/search"
        hx-target="#search-results"
        hx-trigger="input changed delay:300ms"
        autocomplete="off"
        aria-describedby="search-status"
        aria-controls="search-results"
    />

    <div
        id="search-results"
        role="listbox"
        aria-label="Search results"
        aria-live="polite">
        <!-- Results populated by HTMX -->
    </div>

    <div id="search-status" class="sr-only" aria-live="polite" aria-atomic="true">
        <!-- Status announcements -->
    </div>
</div>

Search Result Item

<div role="option" aria-selected="false" class="search-result">
    <a href="{{ result.url }}">
        <span>{{ result.name }}</span>
        <span class="text-muted-foreground">{{ result.type }}</span>
    </a>
</div>

Live Regions

Status Announcements

<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
    {{ status_message }}
</div>

Use aria-live="polite" for non-urgent updates that can wait for user to finish current action.

Alert Messages

<div role="alert" aria-live="assertive">
    <i class="fas fa-exclamation-circle" aria-hidden="true"></i>
    {{ error_message }}
</div>

Use aria-live="assertive" for critical errors that require immediate attention.

Loading State

<div role="status" aria-live="polite" aria-busy="true">
    <span class="sr-only">Loading results...</span>
    <div class="htmx-indicator" aria-hidden="true">
        <i class="fas fa-spinner fa-spin"></i>
    </div>
</div>

Images

Meaningful Image

<img
    src="{{ park.image.url }}"
    alt="{{ park.name }} - {{ park.location }}"
/>

Decorative Image

<img src="/decorative-pattern.svg" alt="" role="presentation" />

Avatar with Name

{% if user.profile.avatar %}
    <img
        src="{{ user.profile.avatar.url }}"
        alt="{{ user.get_full_name|default:user.username }}'s profile picture"
        class="avatar"
    />
{% else %}
    <div class="avatar-placeholder" aria-hidden="true">
        {{ user.username.0|upper }}
    </div>
{% endif %}

Breadcrumbs

{% include 'components/navigation/breadcrumbs.html' %}

The breadcrumbs component automatically provides:

  • <nav> element with aria-label="Breadcrumb"
  • Ordered list for semantic structure
  • aria-current="page" on current page
  • Hidden separators with aria-hidden="true"
  • Schema.org JSON-LD structured data

Add to the top of your base template:

<a href="#main-content" class="skip-link sr-only-focusable">
    Skip to main content
</a>

<!-- ... header and navigation ... -->

<main id="main-content" tabindex="-1">
    {% block content %}{% endblock %}
</main>

Theme Toggle

<button
    @click="toggleTheme()"
    aria-label="Toggle theme"
    :aria-pressed="isDarkMode.toString()"
    class="theme-toggle">
    <i class="fas fa-sun" aria-hidden="true"></i>
    <i class="fas fa-moon" aria-hidden="true"></i>
</button>

Focus Management Utilities

Focus Trap (Alpine.js)

// Already implemented in modal_inner.html
@keydown.tab.prevent="trapFocus($event)"

Return Focus After Action

// Store the trigger element
const trigger = document.activeElement;

// After modal closes
trigger.focus();

Testing Your Components

Quick Accessibility Audit

  1. Can you Tab through all interactive elements?
  2. Is focus indicator visible?
  3. Does Enter/Space activate buttons?
  4. Does Escape close modals/dropdowns?
  5. Does screen reader announce all content?
  6. Is color not the only indicator of state?

Automated Testing

# Run accessibility tests
python manage.py test backend.tests.accessibility

# Use axe browser extension for quick audits
# Install from: https://www.deque.com/axe/