Gallery

A gallery component built without JavaScript.

Image Gallery

Keyboard users: Use Tab to navigate through carousel controls. Use Space or Enter to activate buttons. Arrow keys (←→) navigate between slides when focus is on the carousel. The current slide is announced when changed.

How It Works

This gallery component uses a hybrid approach combining CSS for visual state management with minimal JavaScript for accessibility compliance. It follows the WAI-ARIA Carousel pattern to ensure perfect accessibility for all users.

Core Architecture

The carousel uses hidden radio buttons and CSS :checked selectors for visual state management, while JavaScript handles all accessibility features, ARIA updates, and enhanced keyboard navigation that CSS alone cannot provide.

Why This Hybrid Approach?

  • CSS Handles: Visual transitions, active state styling, responsive design
  • JavaScript Handles: ARIA updates, screen reader announcements, keyboard navigation, focus management
  • Best of Both: Smooth CSS animations with full accessibility compliance

Key Features

  • WAI-ARIA Compliant: Follows official carousel accessibility pattern
  • Dynamic ARIA Updates: JavaScript updates aria-current, aria-label, and announcements
  • Enhanced Keyboard Navigation: Arrow keys, Home/End, Tab navigation
  • Screen Reader Announcements: Real-time slide change announcements
  • Focus Management: Proper focus indicators and keyboard interaction
  • Contextual Button Labels: Prev/next buttons update with current slide context

Accessibility Implementation

This carousel implements comprehensive accessibility features that go far beyond basic compliance. Each feature serves specific user needs and works together to create an inclusive experience for all users, regardless of their abilities or assistive technologies.

ARIA Roles and Semantic Structure

What it does:

  • role="region" on carousel container
  • aria-roledescription="carousel" provides specific component type
  • role="group" on each slide with aria-roledescription="slide"
  • role="group" on slide picker controls

Why it matters:

Screen readers need to understand the structure and purpose of interface elements. Without proper roles, a screen reader might announce this as "list of 6 buttons and 6 images" instead of "carousel with 6 slides." The semantic structure helps users understand they're in a navigation component, not just random content.

User experience:

NVDA announces: "Image gallery carousel, region" - immediately informing users they've entered a carousel component with expected navigation patterns.

Dynamic ARIA States (aria-current)

What it does:

// JavaScript updates current state
slide.setAttribute('aria-current', i === index ? 'true' : 'false');
slidePickerBtn.setAttribute('aria-current', i === index ? 'true' : 'false');

Why it matters:

aria-current="true" is the standard way to indicate the current item in a set. This is crucial for orientation - users need to know where they are in the carousel. Without this, screen reader users would have no way to determine which slide is currently displayed.

User experience:

  • JAWS: "Slide 1 of 6, current, group"
  • VoiceOver: "Slide 1 of 6, current, group, Mountain Peaks"
  • NVDA: "Show slide 1, Mountain landscape, button, current"

Live Region Announcements

What it does:

// Creates temporary announcement
const announcement = `Slide ${index + 1} of ${totalSlides}: ${slideTitle}. ${slideDescription}`;
announcement_element.setAttribute('aria-live', 'polite');
announcement_element.setAttribute('aria-atomic', 'true');

Why it matters:

When slides change, screen reader users need to be informed of the new content. aria-live="polite" ensures announcements don't interrupt the user's current reading, while aria-atomic="true" ensures the entire announcement is read as one unit, not fragmented.

Without this feature:

Users would click next/previous and hear nothing, leaving them confused about whether anything changed. They'd need to manually navigate to the slide content to discover what's now displayed.

User experience:

After clicking "Next": "Slide 2 of 6: Ocean Waves. Powerful waves meeting the rocky coastline" - users immediately understand what changed and where they are.

Contextual Button Labels

What it does:

// Updates button context dynamically
prevBtn.setAttribute('aria-label', 
  `Previous image (currently viewing: ${currentSlideInfo})`);
nextBtn.setAttribute('aria-label', 
  `Next image (currently viewing: ${currentSlideInfo})`);

Why it matters:

Generic "Previous" and "Next" buttons provide minimal context. Enhanced labels give users spatial awareness and help them understand the current state before deciding to navigate. This is especially important for users who might not have heard the initial slide announcement.

Cognitive accessibility benefit:

Users with cognitive disabilities benefit from the extra context, reducing mental load and providing confirmation of current location within the carousel.

User experience:

Tab to Previous button: "Previous image (currently viewing: Mountain Peaks), button" - users know exactly where they are and what will happen if they activate this control.

Position and Context Information

What it does:

  • Each slide labeled as "X of Y" (e.g., "1 of 6")
  • Slide picker buttons include position and content preview
  • Carousel container updates with current slide information

Why it matters:

Spatial orientation is crucial for accessibility. Sighted users can see thumbnails and progress indicators, but screen reader users need explicit position information. This helps users understand the scope of content and their current location.

WCAG 2.1 Compliance:

  • 2.4.6 Headings and Labels: Descriptive labels help users understand purpose
  • 2.4.8 Location: Users can determine their location within the carousel
  • 3.2.4 Consistent Identification: Navigation controls have consistent, predictable behavior

Enhanced Keyboard Navigation

What it does:

// Arrow key navigation
case 'ArrowLeft': goToPrevSlide(); break;
case 'ArrowRight': goToNextSlide(); break;
case 'Home': goToSlide(0); break;
case 'End': goToSlide(totalSlides - 1); break;

Why it matters:

The WAI-ARIA Carousel pattern specifies that arrow keys should navigate between slides when focus is within the carousel. This provides efficient navigation for keyboard users who might otherwise need to tab through individual slide picker buttons.

Motor disability benefits:

Users with limited mobility can navigate the entire carousel with just arrow keys, reducing the number of key presses required. Home/End keys provide quick access to carousel boundaries.

Pattern consistency:

Follows established conventions that users expect from carousel components, reducing cognitive load and leveraging existing user knowledge.

Focus Management and Visual Indicators

What it does:

  • Clear focus rings on all interactive elements
  • Focus remains on activated control after use
  • Focus doesn't get trapped or lost during navigation

Why it matters:

Users with low vision or motor disabilities rely on visible focus indicators to understand which element is active. Proper focus management ensures users don't lose their place when interacting with the carousel.

WCAG 2.1 Compliance:

  • 2.4.7 Focus Visible: Clear visual focus indicators
  • 3.2.1 On Focus: No unexpected context changes when receiving focus
  • 3.2.2 On Input: Predictable behavior when controls are activated

Multi-Modal Accessibility Support

Screen Readers:

  • NVDA, JAWS, VoiceOver all receive proper announcements
  • Role information helps users understand component structure
  • Live regions provide change notifications

Voice Control:

  • Descriptive button labels enable voice commands like "click previous image"
  • Semantic roles help voice software identify interactive elements

Switch Navigation:

  • All controls accessible via sequential Tab navigation
  • No mouse-only interactions required
  • Predictable tab order through carousel controls

Cognitive Accessibility:

  • Consistent interaction patterns
  • Clear labeling with context information
  • Predictable behavior following established conventions

Keyboard Navigation Summary

  • Tab: Navigate between carousel controls (prev, next, slide picker dots)
  • Space/Enter: Activate focused buttons
  • Arrow Left/Right: Navigate between slides when focus is on carousel
  • Home/End: Jump to first/last slide

JavaScript Requirements

While the visual functionality works without JavaScript (thanks to CSS), the accessibility features require minimal JavaScript for:

  1. ARIA Attribute Updates: CSS cannot dynamically update ARIA attributes
  2. Screen Reader Announcements: Proper live region management
  3. Enhanced Keyboard Events: Arrow key navigation and Home/End support
  4. Context-Aware Labels: Dynamic button labels with current slide information

Graceful Degradation

If JavaScript is disabled, the carousel still functions with:

  • Click navigation via slide picker dots
  • Basic keyboard navigation (Tab + Space)
  • Visual state management via CSS
  • All images and content remain accessible

Implementation Code

HTML Structure

<!-- Carousel with proper ARIA structure -->
<section role="region" aria-label="Image gallery carousel" aria-roledescription="carousel">
  <!-- Hidden radio buttons for CSS state management -->
  <input type="radio" name="gallery" id="img1" checked>
  
  <!-- Carousel controls -->
  <button class="carousel-btn carousel-btn-prev" aria-label="Previous image">‹</button>
  <button class="carousel-btn carousel-btn-next" aria-label="Next image">›</button>
  
  <!-- Slides with proper roles -->
  <div class="slide" role="group" aria-roledescription="slide" aria-label="1 of 6">
    <img src="image.jpg" alt="Descriptive alt text" />
  </div>
  
  <!-- Slide picker controls -->
  <button class="slide-picker-btn" aria-current="true"></button>
</section>

CSS State Management

/* Hide all slides by default */
.slide {
  opacity: 0;
  transform: scale(0.95);
  transition: all 0.5s ease;
}

/* Show active slide when radio is checked */
#img1:checked ~ .gallery-slides .slide[data-slide="1"] {
  opacity: 1;
  transform: scale(1);
}

/* Active slide picker styling */
.slide-picker-btn[aria-current="true"] {
  background: var(--primary);
  transform: scale(1.3);
}

JavaScript Accessibility Enhancement

// Update ARIA states dynamically
function updateCarousel(index) {
  // Update slides
  slides.forEach((slide, i) => {
    slide.setAttribute('aria-current', i === index ? 'true' : 'false');
    // Hide inactive slides from screen readers
    if (i === index) {
      slide.removeAttribute("aria-hidden");
    } else {
      slide.setAttribute("aria-hidden", "true");
    }
  });
  
  // Update button context
  const currentSlideInfo = slides[index].querySelector('h4').textContent;
  prevBtn.setAttribute('aria-label', 
    `Previous image (currently viewing: ${currentSlideInfo})`);
}

// Announce changes to screen readers
function announceSlideChange(index) {
  const announcement = `Slide ${index + 1} of ${totalSlides}: ${slideTitle}`;
  // Create temporary live region for screen reader announcement
}

Additional Resources