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 witharia-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:
- ARIA Attribute Updates: CSS cannot dynamically update ARIA attributes
- Screen Reader Announcements: Proper live region management
- Enhanced Keyboard Events: Arrow key navigation and Home/End support
- 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
}