CrackedRuby CrackedRuby

Overview

Accessibility standards define technical requirements and guidelines for creating software that people with disabilities can use effectively. These standards address visual, auditory, motor, and cognitive impairments through specific technical implementations. The Web Content Accessibility Guidelines (WCAG) serve as the international standard, while Section 508 governs U.S. federal websites and applications.

Software accessibility extends beyond legal compliance to fundamental usability. Applications that meet accessibility standards work correctly with screen readers, keyboard navigation, voice control systems, and other assistive technologies. Standards specify requirements for contrast ratios, text alternatives, keyboard operability, and predictable behavior.

WCAG organizes requirements into four principles: perceivable, operable, understandable, and robust. Each principle contains specific guidelines with testable success criteria at three conformance levels: A (minimum), AA (mid-range), and AAA (highest). Most organizations target WCAG 2.1 Level AA compliance, which balances comprehensive accessibility with practical implementation.

# Basic accessible form in Rails
<%= form_with model: @user do |form| %>
  <div>
    <%= form.label :email, "Email Address" %>
    <%= form.email_field :email, 
      aria: { describedby: "email-hint", required: "true" } %>
    <span id="email-hint">We'll never share your email</span>
  </div>
  
  <%= form.submit "Sign Up", 
    aria: { label: "Submit registration form" } %>
<% end %>

The standards apply to web applications, mobile apps, desktop software, and embedded systems. Each platform has specific implementation requirements, but underlying principles remain consistent. Accessibility requirements affect HTML structure, CSS styling, JavaScript interactions, form design, media content, and navigation patterns.

Key Principles

WCAG's four foundational principles establish the framework for accessible software design. These principles apply regardless of technology stack or platform.

Perceivable requires that users can perceive information through available senses. Content must not be invisible to all user senses. This includes text alternatives for images, captions for audio, adaptable content structure, and distinguishable foreground/background elements. Color cannot be the only means of conveying information. Sufficient contrast ratios ensure text remains readable against backgrounds.

# Perceivable image with text alternative
<%= image_tag "chart.png", 
  alt: "Sales increased 40% in Q4, from $2M to $2.8M" %>

# Insufficient - decorative image needs empty alt
<%= image_tag "decorative-border.png", alt: "" %>

Operable ensures users can operate interface components and navigation. All functionality must be available via keyboard. Users need sufficient time to read and complete tasks. Content must not trigger seizures through flashing. Navigation mechanisms must help users find content and determine their location.

Keyboard operability requires logical tab order, visible focus indicators, and keyboard shortcuts that don't conflict with assistive technology. Time limits need adjustment or elimination mechanisms. Flashing content must stay below threshold frequencies.

# Skip navigation link for keyboard users
<%= link_to "Skip to main content", "#main-content", 
  class: "skip-link",
  accesskey: "s" %>

<main id="main-content" tabindex="-1">
  <%= yield %>
</main>

Understandable mandates that users can understand information and interface operation. Text must be readable and predictable. Input assistance helps users avoid and correct errors. Pages must appear and operate in predictable ways.

Readability requires clear language, pronunciation guidance for abbreviations, and text appropriate for the reading level. Predictability means consistent navigation, consistent identification, and avoiding changes on focus. Error prevention and correction require clear error messages, suggestions, and confirmation for important actions.

Robust requires content interpretation by various user agents, including assistive technologies. As technologies evolve, content must remain accessible. This principle emphasizes valid HTML, proper ARIA usage, and status message communication to assistive technology.

Parsing requirements mandate proper element nesting, unique IDs, and complete start/end tags. Name, role, and value must be programmatically determinable for all interface components. Status messages must communicate to assistive technology without receiving focus.

Ruby Implementation

Rails provides built-in helpers that generate accessible HTML when used correctly. The framework includes ARIA attribute support, semantic HTML generation, and form helpers with proper labeling mechanisms.

Form helpers automatically associate labels with inputs through for and id attributes. The form.label method generates properly connected label elements. Custom ARIA attributes extend semantic information for complex interactions.

# Accessible form with error handling
class RegistrationForm
  include ActiveModel::Model
  
  attr_accessor :email, :password, :age
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, length: { minimum: 8 }
  validates :age, numericality: { greater_than_or_equal_to: 13 }
end

# In view
<%= form_with model: @registration_form, 
  url: register_path,
  html: { novalidate: true } do |form| %>
  
  <% if @registration_form.errors.any? %>
    <div role="alert" aria-live="assertive" class="error-summary">
      <h2 id="error-heading">
        <%= pluralize(@registration_form.errors.count, "error") %> 
        prevented registration
      </h2>
      <ul aria-labelledby="error-heading">
        <% @registration_form.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  
  <div class="field">
    <%= form.label :email, class: "required" %>
    <%= form.email_field :email,
      aria: { 
        required: "true",
        invalid: @registration_form.errors[:email].present?,
        describedby: "email-hint email-error"
      } %>
    <span id="email-hint" class="hint">Enter a valid email address</span>
    <% if @registration_form.errors[:email].present? %>
      <span id="email-error" class="error" role="alert">
        <%= @registration_form.errors[:email].join(", ") %>
      </span>
    <% end %>
  </div>
<% end %>

The active_text gem provides accessible rich text editing. It generates proper heading hierarchy, maintains semantic structure, and supports keyboard navigation through editor controls. Image uploads require alt text input, enforcing text alternatives at content creation.

# Accessible rich text editor
class Article < ApplicationRecord
  has_rich_text :body
  
  validates :body, presence: true
  validate :validate_image_alt_text
  
  private
  
  def validate_image_alt_text
    return unless body.present?
    
    body.body.attachables.select { |a| a.is_a?(ActiveStorage::Blob) }.each do |blob|
      next unless blob.content_type.start_with?("image/")
      
      attachment = body.body.attachments.find { |a| a.blob_id == blob.id }
      if attachment && attachment.caption.blank?
        errors.add(:body, "All images require descriptive alt text")
      end
    end
  end
end

Rails routing supports multiple formats, allowing content adaptation for different user needs. JSON APIs enable custom interface implementations optimized for assistive technology. Content negotiation serves appropriate formats based on request headers.

# Controller supporting multiple accessible formats
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    
    respond_to do |format|
      format.html # Standard web view
      format.json # Screen reader optimized data
      format.text # Plain text for speech synthesis
      format.pdf  # High contrast print version
    end
  end
end

Action Cable enables real-time updates with proper ARIA live region support. Dynamic content changes must announce to screen readers without disrupting current focus or navigation context.

# Accessible real-time notifications
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications_#{current_user.id}"
  end
end

# Client-side announcement
consumer.subscriptions.create("NotificationsChannel", {
  received(data) {
    const announcement = document.getElementById('aria-announcements');
    announcement.textContent = data.message;
    // Live region announces without moving focus
  }
});

Implementation Approaches

Three primary approaches address accessibility requirements: progressive enhancement, inclusive design from inception, and retrofitting existing applications. Each approach involves different trade-offs in development time, code complexity, and outcome quality.

Progressive Enhancement builds core functionality with semantic HTML, then layers interactive features. The base layer works without JavaScript, ensuring assistive technology compatibility. Enhanced features detect support before activation. This approach creates naturally accessible foundations.

Start with server-rendered HTML forms that submit without JavaScript. Add client-side validation that enhances server validation. Implement AJAX submissions that fall back to standard form posts. Each enhancement layer maintains accessibility.

# Progressive enhancement pattern
class SearchController < ApplicationController
  def index
    @results = search_service.search(params[:q]) if params[:q].present?
    
    respond_to do |format|
      format.html # Server-rendered results page
      format.json { render json: @results } # AJAX enhancement
    end
  end
end

# View with progressive enhancement
<%= form_with url: search_path, 
  method: :get,
  data: { remote: false, controller: "live-search" } do |form| %>
  <%= form.search_field :q, 
    placeholder: "Search articles",
    data: { action: "input->live-search#search" },
    aria: { label: "Search", live: "polite" } %>
  <%= form.submit "Search", data: { live_search_target: "submit" } %>
<% end %>

<div data-live-search-target="results" role="region" aria-live="polite">
  <%= render @results if @results %>
</div>

Inclusive Design integrates accessibility from project inception. Requirements gathering includes assistive technology users. Design mockups specify semantic structure, focus management, and keyboard interactions. Development includes accessibility testing in each sprint.

Create component libraries with accessibility built in. Document keyboard interactions, ARIA patterns, and screen reader behavior for each component. Establish accessibility acceptance criteria for every user story.

# Accessible component design pattern
module AccessibleComponents
  class TabsComponent < ViewComponent::Base
    def initialize(tabs:, selected: 0)
      @tabs = tabs
      @selected = selected
    end
    
    def before_render
      @tabs.each_with_index do |tab, index|
        tab[:id] ||= "tab-#{SecureRandom.hex(4)}"
        tab[:panel_id] ||= "panel-#{SecureRandom.hex(4)}"
        tab[:selected] = (index == @selected)
      end
    end
  end
end

# Component template with full keyboard support
<div class="tabs" data-controller="tabs">
  <div role="tablist" aria-label="<%= @label %>">
    <% @tabs.each_with_index do |tab, index| %>
      <%= button_tag tab[:label],
        role: "tab",
        aria: {
          selected: tab[:selected],
          controls: tab[:panel_id]
        },
        id: tab[:id],
        tabindex: tab[:selected] ? 0 : -1,
        data: { 
          action: "click->tabs#select keydown->tabs#navigate",
          tabs_target: "tab"
        } %>
    <% end %>
  </div>
  
  <% @tabs.each do |tab| %>
    <div role="tabpanel"
      id="<%= tab[:panel_id] %>"
      aria-labelledby="<%= tab[:id] %>"
      hidden="<%= !tab[:selected] %>"
      tabindex="0"
      data-tabs-target="panel">
      <%= tab[:content] %>
    </div>
  <% end %>
</div>

Retrofitting adds accessibility to existing applications. Automated scanning identifies technical violations. Manual testing uncovers usability issues. Prioritization addresses critical barriers first, then progressive improvements.

Begin with automated tools to find missing alt text, color contrast issues, and structural problems. Conduct keyboard navigation testing to identify focus traps and navigation barriers. Test with actual screen readers to discover interaction problems. Create prioritized remediation backlog based on impact and frequency.

# Accessibility audit rake task
namespace :accessibility do
  desc "Audit application for common accessibility issues"
  task audit: :environment do
    issues = []
    
    # Check for images without alt text
    ApplicationRecord.descendants.each do |model|
      next unless model.respond_to?(:has_one_attached) || 
                  model.respond_to?(:has_many_attached)
      
      model.attachment_reflections.each do |name, reflection|
        model.find_each do |record|
          attachments = record.send(name)
          Array(attachments).each do |attachment|
            if attachment.blob.content_type.start_with?("image/") &&
               attachment.caption.blank?
              issues << "#{model.name}##{record.id}: #{name} missing alt text"
            end
          end
        end
      end
    end
    
    puts "Found #{issues.count} accessibility issues:"
    issues.each { |issue| puts "  - #{issue}" }
  end
end

Practical Examples

Accessible data tables require proper structure, column headers, and row headers for screen reader navigation. Complex tables need scope attributes and caption elements. Sortable tables must announce sort order changes.

# Accessible data table with sorting
class UsersController < ApplicationController
  def index
    @users = User.all
    @users = @users.order(params[:sort]) if params[:sort].present?
    @sort_column = params[:sort]&.split(" ")&.first
    @sort_direction = params[:sort]&.split(" ")&.last || "asc"
  end
end

# View template
<table role="table" aria-label="User directory">
  <caption class="sr-only">
    List of <%= @users.count %> users, 
    sorted by <%= @sort_column || "registration date" %> 
    <%= @sort_direction %>
  </caption>
  
  <thead>
    <tr>
      <th scope="col">
        <%= link_to "Name", 
          users_path(sort: "name #{toggle_direction(@sort_column, 'name', @sort_direction)}"),
          aria: { sort: sort_aria(@sort_column, 'name', @sort_direction) } %>
      </th>
      <th scope="col">
        <%= link_to "Email",
          users_path(sort: "email #{toggle_direction(@sort_column, 'email', @sort_direction)}"),
          aria: { sort: sort_aria(@sort_column, 'email', @sort_direction) } %>
      </th>
      <th scope="col">Role</th>
      <th scope="col">Actions</th>
    </tr>
  </thead>
  
  <tbody>
    <% @users.each do |user| %>
      <tr>
        <th scope="row"><%= user.name %></th>
        <td><%= user.email %></td>
        <td><%= user.role %></td>
        <td>
          <%= link_to "Edit", edit_user_path(user), 
            aria: { label: "Edit #{user.name}" } %>
          <%= button_to "Delete", user_path(user), 
            method: :delete,
            form: { data: { turbo_confirm: "Delete #{user.name}?" } },
            aria: { label: "Delete #{user.name}" } %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

Modal dialogs require focus management, keyboard trapping, and proper ARIA attributes. Opening a modal moves focus inside. Escape key closes the modal. Focus returns to the trigger element on close.

# Accessible modal implementation
class ModalComponent < ViewComponent::Base
  def initialize(title:, id: nil, size: :medium)
    @title = title
    @id = id || "modal-#{SecureRandom.hex(4)}"
    @size = size
  end
  
  def title_id
    "#{@id}-title"
  end
  
  def description_id
    "#{@id}-description"
  end
end

# Component template
<div id="<%= @id %>"
  class="modal"
  role="dialog"
  aria-modal="true"
  aria-labelledby="<%= title_id %>"
  aria-describedby="<%= description_id %>"
  data-controller="modal"
  data-modal-target="container"
  hidden>
  
  <div class="modal-backdrop" 
    data-action="click->modal#close"
    aria-hidden="true">
  </div>
  
  <div class="modal-content" role="document">
    <header class="modal-header">
      <h2 id="<%= title_id %>"><%= @title %></h2>
      <%= button_tag "×", 
        class: "modal-close",
        aria: { label: "Close dialog" },
        data: { action: "modal#close" } %>
    </header>
    
    <div id="<%= description_id %>" class="modal-body">
      <%= content %>
    </div>
    
    <footer class="modal-footer">
      <%= button_tag "Cancel", 
        data: { action: "modal#close" },
        class: "button-secondary" %>
      <%= button_tag "Confirm",
        data: { action: "modal#confirm" },
        class: "button-primary" %>
    </footer>
  </div>
</div>

Autocomplete search requires live region announcements, keyboard navigation through results, and result count communication. Arrow keys navigate suggestions. Enter selects. Escape clears.

# Accessible autocomplete search
class SearchController < ApplicationController
  def suggestions
    query = params[:q]
    @suggestions = SearchService.autocomplete(query).limit(10)
    
    render json: {
      suggestions: @suggestions.map { |s| 
        { id: s.id, label: s.title, category: s.category }
      },
      count: @suggestions.count,
      announcement: "#{@suggestions.count} suggestions available"
    }
  end
end

# Stimulus controller for keyboard navigation
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "results", "announcement"]
  
  connect() {
    this.selectedIndex = -1
  }
  
  async search() {
    const query = this.inputTarget.value
    if (query.length < 2) {
      this.hideResults()
      return
    }
    
    const response = await fetch(`/search/suggestions?q=${encodeURIComponent(query)}`)
    const data = await response.json()
    
    this.displayResults(data.suggestions)
    this.announce(data.announcement)
  }
  
  navigate(event) {
    const suggestions = this.resultsTarget.querySelectorAll('[role="option"]')
    
    switch(event.key) {
      case 'ArrowDown':
        event.preventDefault()
        this.selectedIndex = Math.min(this.selectedIndex + 1, suggestions.length - 1)
        this.updateSelection(suggestions)
        break
      case 'ArrowUp':
        event.preventDefault()
        this.selectedIndex = Math.max(this.selectedIndex - 1, -1)
        this.updateSelection(suggestions)
        break
      case 'Enter':
        event.preventDefault()
        if (this.selectedIndex >= 0) {
          suggestions[this.selectedIndex].click()
        }
        break
      case 'Escape':
        this.hideResults()
        break
    }
  }
  
  updateSelection(suggestions) {
    suggestions.forEach((suggestion, index) => {
      const isSelected = index === this.selectedIndex
      suggestion.setAttribute('aria-selected', isSelected)
      if (isSelected) {
        this.inputTarget.setAttribute('aria-activedescendant', suggestion.id)
      }
    })
  }
  
  announce(message) {
    this.announcementTarget.textContent = message
  }
}

Testing Approaches

Automated testing catches structural and semantic issues. Pa11y, axe-core, and Lighthouse audit pages for WCAG violations. Integration tests verify keyboard navigation and ARIA attribute presence. Manual testing with assistive technology validates actual user experience.

Unit tests verify component accessibility attributes. System tests check keyboard navigation paths. JavaScript tests confirm dynamic content announcements.

# RSpec accessibility tests
require 'rails_helper'

RSpec.describe "User Registration", type: :system do
  it "provides accessible form with proper labels and hints" do
    visit new_user_registration_path
    
    # Check semantic structure
    expect(page).to have_css('form')
    expect(page).to have_css('label[for="user_email"]', text: 'Email')
    expect(page).to have_css('input#user_email[type="email"]')
    
    # Verify ARIA attributes
    email_field = find('#user_email')
    expect(email_field['aria-required']).to eq('true')
    expect(email_field['aria-describedby']).to include('email-hint')
  end
  
  it "announces errors to screen readers" do
    visit new_user_registration_path
    click_button 'Sign Up'
    
    # Error summary with alert role
    expect(page).to have_css('[role="alert"]')
    
    # Specific field errors linked to inputs
    expect(page).to have_css('#user_email[aria-invalid="true"]')
    error_id = find('#user_email')['aria-describedby']
    expect(page).to have_css("##{error_id}", text: "can't be blank")
  end
  
  it "supports complete keyboard navigation" do
    visit new_user_registration_path
    
    # Tab through form fields
    find('body').send_keys(:tab) # Focus on first field
    expect(page).to have_css('#user_email:focus')
    
    find('body').send_keys(:tab)
    expect(page).to have_css('#user_password:focus')
    
    find('body').send_keys(:tab)
    expect(page).to have_css('input[type="submit"]:focus')
    
    # Submit with keyboard
    find('input[type="submit"]').send_keys(:enter)
    expect(page).to have_current_path(new_user_registration_path)
  end
end

# Component accessibility specs
RSpec.describe AccessibleComponents::TabsComponent, type: :component do
  it "renders with proper ARIA tablist structure" do
    tabs = [
      { label: "Profile", content: "Profile content" },
      { label: "Settings", content: "Settings content" }
    ]
    
    render_inline(described_class.new(tabs: tabs))
    
    expect(page).to have_css('[role="tablist"]')
    expect(page).to have_css('[role="tab"]', count: 2)
    expect(page).to have_css('[role="tabpanel"]', count: 2)
    
    # First tab selected by default
    first_tab = page.all('[role="tab"]').first
    expect(first_tab['aria-selected']).to eq('true')
    expect(first_tab['tabindex']).to eq('0')
    
    # Other tabs not selected
    second_tab = page.all('[role="tab"]').last
    expect(second_tab['aria-selected']).to eq('false')
    expect(second_tab['tabindex']).to eq('-1')
  end
end

Axe-core integration provides automated WCAG violation detection during test runs. Configure custom rules for organization-specific requirements.

# axe-core integration
require 'capybara/rspec'
require 'axe-capybara'

RSpec.configure do |config|
  config.after(:each, type: :system) do
    # Run axe accessibility audit on every system test
    violations = page.axe.violations
    
    if violations.any?
      violations.each do |violation|
        puts "\n#{violation.impact.upcase}: #{violation.help}"
        puts "  #{violation.help_url}"
        violation.nodes.each do |node|
          puts "  - #{node.target.join(', ')}"
          puts "    #{node.failure_summary}"
        end
      end
      
      fail "Page has #{violations.count} accessibility violations"
    end
  end
end

Screen reader testing requires actual assistive technology. Test with NVDA, JAWS, and VoiceOver. Document expected screen reader output. Verify announcements match user expectations.

# Screen reader testing checklist
class AccessibilityTestPlan
  SCREEN_READER_TESTS = {
    forms: [
      "Label announces before input",
      "Required fields identified",
      "Error messages linked and announced",
      "Hint text provided before input",
      "Submit button clearly identified"
    ],
    navigation: [
      "Skip link available and functional",
      "Heading hierarchy logical",
      "Landmark regions identified",
      "Current page identified in navigation",
      "Breadcrumb trail available"
    ],
    dynamic_content: [
      "Loading states announced",
      "Content updates announced without focus change",
      "Error messages announced immediately",
      "Success confirmations announced",
      "Progress indicators communicate status"
    ]
  }
end

Tools & Ecosystem

Ruby accessibility tools span static analysis, runtime testing, and continuous monitoring. The ecosystem includes linters, testing frameworks, and browser extensions.

axe-matchers provides RSpec matchers for accessibility testing. Add to Gemfile and configure RSpec helpers. Run audits automatically during feature tests.

# Gemfile
gem 'axe-matchers', group: :test

# rails_helper.rb
require 'axe/matchers'

RSpec.configure do |config|
  config.include Axe::Matchers
end

# Usage in specs
RSpec.describe "Dashboard", type: :feature do
  it "meets accessibility standards" do
    visit dashboard_path
    expect(page).to be_axe_clean
  end
  
  it "meets WCAG AA standards" do
    visit dashboard_path
    expect(page).to be_axe_clean.according_to(:wcag2aa)
  end
  
  it "allows specific violations to be excluded temporarily" do
    visit dashboard_path
    expect(page).to be_axe_clean.excluding('#legacy-widget')
  end
end

erb-lint catches accessibility issues in Rails templates. Configure rules for missing alt text, improper heading hierarchy, and interactive element requirements.

# .erb-lint.yml
linters:
  RequireImageAlt:
    enabled: true
  RequireInputAutocomplete:
    enabled: true
  NoJavaScriptTagHelper:
    enabled: true
    
# Run linter
$ bundle exec erblint --lint-all

rack-reducer and rack-cors configuration affects API accessibility. Proper content negotiation enables alternative formats for assistive technology.

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*', 
          headers: :any,
          methods: [:get, :post, :options],
          expose: ['Content-Type', 'Accept-Language']
      end
    end
  end
end

capybara-accessible adds accessibility assertions to Capybara tests. Verify semantic HTML, keyboard navigation, and ARIA patterns.

gem 'capybara-accessible'

# Enable in tests
require 'capybara/accessible'

RSpec.configure do |config|
  config.include Capybara::Accessible
  
  config.after(:each, type: :feature) do
    unless page.accessible?
      violations = page.accessibility_violations
      fail "Accessibility violations found: #{violations.inspect}"
    end
  end
end

Pa11y provides command-line accessibility testing. Run against development, staging, and production environments. Integrate into CI/CD pipelines.

# Install Pa11y
$ npm install -g pa11y

# Test single page
$ pa11y http://localhost:3000

# Test with specific standard
$ pa11y --standard WCAG2AA http://localhost:3000

# Generate reports
$ pa11y --reporter json http://localhost:3000 > report.json

Browser extensions assist manual testing. WAVE identifies issues visually. Accessibility Insights provides guided assessments. Screen reader tools simulate assistive technology.

Common Pitfalls

Developers frequently implement visual accessibility without semantic structure. Using div elements with click handlers instead of button elements breaks keyboard navigation and screen reader functionality. Buttons provide built-in keyboard support, focus management, and semantic meaning.

# Incorrect - div as button
<div class="button" onclick="submitForm()">Submit</div>

# Correct - semantic button
<%= button_tag "Submit", 
  type: "submit",
  data: { action: "click->form#submit" } %>

Color alone cannot convey information. Required form fields marked only with red asterisks fail for colorblind users. Combine color with text, icons, or patterns.

# Incorrect - color only
<%= form.label :email %>
<span style="color: red;">*</span>

# Correct - text and visual indicator
<%= form.label :email, class: "required" do %>
  Email <abbr title="required" aria-label="required">*</abbr>
<% end %>

Missing form labels create navigation barriers. Placeholder text disappears on input and cannot serve as labels. Every input requires an associated label element or aria-label attribute.

# Incorrect - placeholder without label
<%= form.text_field :search, placeholder: "Search..." %>

# Correct - label with optional placeholder
<%= form.label :search, "Search", class: "sr-only" %>
<%= form.text_field :search, 
  placeholder: "Enter keywords",
  aria: { label: "Search" } %>

Dynamic content updates without announcements confuse screen reader users. Loading new content, showing validation errors, or updating status requires ARIA live regions.

# Incorrect - silent content update
<div id="search-results">
  <%= render @results %>
</div>

# Correct - announced content update
<div id="search-results" 
  role="region"
  aria-live="polite"
  aria-relevant="additions removals">
  <%= render @results %>
</div>

<div aria-live="polite" aria-atomic="true" class="sr-only">
  <%= "Found #{@results.count} results" if @results %>
</div>

Focus management errors trap keyboard users or lose focus position. Modal dialogs must trap focus inside. Closing modals must return focus to trigger elements. Deleted items must move focus to logical alternatives.

# Focus management after deletion
def destroy
  @item = Item.find(params[:id])
  @item.destroy
  
  respond_to do |format|
    format.html { 
      redirect_to items_path, 
        notice: "Item deleted",
        status: :see_other
    }
    format.turbo_stream {
      render turbo_stream: [
        turbo_stream.remove(@item),
        turbo_stream.update("notifications", 
          partial: "shared/notification",
          locals: { 
            message: "Item deleted",
            focus_target: "items-table" 
          })
      ]
    }
  end
end

Insufficient contrast ratios reduce readability for users with visual impairments. Text must meet 4.5:1 contrast ratio for normal text, 3:1 for large text. Test color combinations with contrast checking tools.

Images without alternative text exclude screen reader users from content. Decorative images need empty alt attributes. Informative images require descriptive alt text. Complex images need extended descriptions.

# Decorative image
<%= image_tag "decorative-border.png", alt: "", role: "presentation" %>

# Informative image
<%= image_tag "save-icon.png", alt: "Save document" %>

# Complex image
<%= image_tag "sales-chart.png",
  alt: "Sales chart",
  aria: { describedby: "chart-description" } %>
<div id="chart-description" class="sr-only">
  Sales increased from $2M in Q1 to $3.2M in Q4, 
  with steady growth each quarter.
</div>

Custom controls without keyboard support exclude keyboard users. Implement arrow key navigation for custom selects, tab lists, and menus. Add space/enter activation for custom buttons and checkboxes.

Time limits without adjustment mechanisms create barriers. Provide mechanisms to extend, disable, or adjust time limits. Warn users before expiration with sufficient time to respond.

Reference

WCAG Conformance Levels

Level Requirements Typical Use
A Basic accessibility Minimum legal compliance
AA Addresses major barriers Industry standard target
AAA Enhanced accessibility Specialized applications

Essential ARIA Roles

Role Purpose Required States
button Identifies clickable control aria-pressed for toggles
dialog Modal or non-modal dialog aria-modal, aria-labelledby
tablist Container for tabs Contains tab roles
tab Individual tab control aria-selected, aria-controls
tabpanel Tab content area aria-labelledby
alert Important message aria-live is implicit
status Advisory information aria-live polite implicit
navigation Navigation landmark aria-label if multiple
main Primary content Only one per page
complementary Supporting content aria-label if multiple
search Search facility Contains search form

Required HTML Attributes

Element Required Attributes Purpose
img alt Text alternative
input id, associated label Form control identification
button type Prevent default submit
iframe title Frame purpose
table caption or aria-label Table description
th scope Header association
fieldset legend Group description

Color Contrast Requirements

Content Type Level AA Level AAA
Normal text 4.5:1 7:1
Large text (18pt+) 3:1 4.5:1
UI components 3:1 Enhanced perception
Focus indicators 3:1 4.5:1 recommended

Keyboard Navigation Patterns

Pattern Keys Behavior
Tab Tab, Shift+Tab Move between controls
Menu Arrow keys Navigate items
Dialog Escape Close dialog
Tabs Arrow keys Switch tabs
Autocomplete Arrow keys, Enter Navigate, select suggestions
Modal Tab cycles within Focus trapped

ARIA Live Region Settings

Setting Announcement Timing Use Case
off No announcement Default state
polite After current speech Status updates
assertive Interrupts current speech Critical alerts

Common ARIA Attributes

Attribute Values Purpose
aria-label String Accessible name
aria-describedby ID reference Extended description
aria-labelledby ID reference Label association
aria-required true, false Required field
aria-invalid true, false Validation state
aria-expanded true, false Disclosure state
aria-hidden true, false Hide from assistive tech
aria-live off, polite, assertive Update announcements
aria-selected true, false Selection state
aria-checked true, false, mixed Checkbox state

Rails Accessibility Helpers

Helper Accessibility Feature
form.label Associated labels
link_to Semantic link elements
button_to Proper button elements
form_with ARIA attributes support
image_tag Alt text parameter
content_tag Custom ARIA attributes

Testing Checklist

Area Validation Method
Keyboard navigation Manual testing with Tab key
Screen reader NVDA, JAWS, VoiceOver testing
Color contrast Automated contrast checker
HTML validity W3C validator
ARIA implementation axe DevTools
Focus management Visual focus indicator testing
Form labels Automated linter checking
Alternative text Manual content review
Heading structure Document outline review