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 |