Cross-Browser Animation Challenges in Carousels with Accessibility in Mind

Cross-Browser Animation Challenges in Carousels with Accessibility in Mind

1️⃣ Context

Carousels are a common and visually appealing way to display content on websites. However, creating a truly cross-browser and accessible carousel from scratch is more challenging than it may seem. My goal was to create a carousel with an infinite scroll effect, seamless animations, and robust accessibility — supporting features like tab navigation, Voiceover, text zoom, and keyboard interaction, while ensuring consistent behavior across browsers. I do not aim to walk you through building a carousel from scratch. Instead, I’ll focus on key accessibility features and cross-browser issues I encountered during development. I’ll also share uncommon solutions that proved effective in overcoming these challenges.

2️⃣ Requirements explained:

To build a truly cross-browser accessible carousel, several key requirements must be met to ensure usability, performance, and accessibility. Here’s a breakdown of the essential features:

🔄 Keyboard Navigation:

Users should be able to navigate through the carousel using the Tab key and arrow keys (← / →). Each interactive element (like dots, paddle buttons, and slides) should be part of the tab order and accessible in the correct logical sequence. The HTML structure above reflects this behavior, ensuring users can navigate correctly.

🗣️ Screen Reader Support:

The carousel should be fully compatible with screen readers like VoiceOver. This includes ensuring that ARIA attributes like aria-label, aria-controls, and aria-current are correctly applied to each relevant element. Without proper ARIA roles and attributes, users relying on assistive technology struggle to understand or interact with the carousel

🎞️ Smooth Animations:

Animations must be smooth and responsive across devices, especially on mobile where limited system resources can cause janky transitions. Techniques like GPU acceleration can help to guarantee sliding transitions, even on lower-end devices.

🔁 Infinite Scroll:

The carousel should have a circular navigation system, allowing users to loop from the last slide back to the first, and vice versa. This provides a seamless user experience and eliminates the “dead-end” feeling of a traditional linear carousel.

🔍 Text Zoom Support:

When users zoom text, the carousel layout should stay intact. Text should not be cut off or overflowed outside of its container. This ensures accessibility for users with visual impairments;

Safari-Specific Issues

  1. Animation Glitches: Tab navigation and voice-over on Safari cause stuttering animations or skipped animations.

  2. Smoothness Issues: Mobile Safari exhibits janky animations, especially when transform-based animations are used

3️⃣ Solution

CSS Solutions Explored:

  • Multi-Layer GPU Transforms:

    When using VoiceOver and tab navigation, Safari encountered layout issues mid-animation, while Chrome performed smoothly. After some investigation, I discovered that the transform: translateZ() property creates a GPU layer, significantly improving animation smoothness. On desktop browsers, this worked perfectly — no layout breaking and smooth animations.

    However, on mobile devices, while the layout no longer broke, the animations became laggy. This was due to the limited resources on mobile devices, making the extra GPU layer a heavier task to handle.

      .carousel-item {
        transform: translateZ(0); /*  creates a new compositing layer on the GPU. */ 
        -webkit-transform: translateZ(0); /* For older browsers */
      }
    
  • Property will-change for Animation Hints:

    Browsers don't pre-optimize animations without knowing what will change. Add will-change: transform; to hint that the transform property will be animated. This simple change made a huge difference in the smoothness of animations on Safari.

      .carousel-item {
        will-change: transform; /* prepare the browser to reduce the cost of recalculations */
      }
    
  • Migrate from position: absolute to display: grid

    While investigating why Chrome animations worked perfectly but Safari didn't, I learned that Safari has issues managing position: absolute during animations. When elements were animating with position: absolute, it would cause layout shifts and reflows, especially when dealing with overflowing elements. The solution was to switch from position: absolute to display: grid, which is less likely to trigger layout recalculations;

      .carousel {
        display: grid;
        grid-template-columns: repeat(5, 100%);
        overflow: hidden;
      }
    

By combining only the last two solutions — will-change optimization and grid layout — the animations achieved cross-browser consistency in Safari and Chrome. While the GPU transform worked well on desktop, it didn’t deliver the same smooth experience on mobile in my scenario.

Accessibility Features Explained

A truly accessible carousel goes beyond smooth animations. It ensures that users with disabilities can navigate with a keyboard and understand the context using assistive technologies like VoiceOver. Below is the complete HTML structure with all the correct relationships in place. After that, I’ll highlight key elements and explain their functionalities, such as ARIA attributes and the logical order of elements. To make the explanations more didactic, I’ve included variables in the HTML to illustrate key concepts. In a real-world scenario, these values should be dynamically generated by the JavaScript or framework you are using.

HTML Example:

<div class="carousel" aria-label="Carousel group name">

  <!-- Dot Navigation -->
  <div class="dotnav" aria-label="Slide navigation controls">
    <button class="dot" aria-label="Go to slide 1" aria-controls="carousel-item-${curent_index}" aria-current="true"></button>
    <button class="dot" aria-label="Go to slide 2" aria-controls="carousel-item-${curent_index}"></button>
    <button class="dot" aria-label="Go to slide 3" aria-controls="carousel-item-${curent_index}"></button>
  </div>

  <!-- Carousel Items -->
  <ul class="carousel-items" role="tablist" aria-live="polite">
    <li class="carousel-item" role="tabpanel" id="carousel-item-1" tabindex="0" aria-label="Title of slide 1">
      ..
    </li>
    <li class="carousel-item" role="tabpanel" id="carousel-item-2" tabindex="0" aria-label="Title of slide 2">
      ..
    </li>
    <li class="carousel-item" role="tabpanel" id="carousel-item-3" tabindex="0" aria-label="Title of slide 3">
      ...
    </li>
  </ul>

  <!-- Paddle Navigation -->
  <div class="paddlenavs-container">
    <button class="paddle-nav prev" aria-label="Previous slide" aria-controls="carousel-item-${curent_index}">
      Previous
    </button>
    <button class="paddle-nav next" aria-label="Next slide" aria-controls="carousel-item-${curent_index}">
      Next
    </button>
  </div>

</div>

Aria attributes:

  • role="tablist": Declares that the container holds a group of tabs (in this case, the slides) that users can navigate between.

    role="tabpanel": Identifies each slide as part of a group of tabbed panels, allowing assistive technologies to recognize the relationship between the slides.

    aria-label: Describes the element for screen readers, often used to provide context, such as the title or description of a slide.

    aria-controls: Links navigation buttons (like dots or paddles) to the corresponding carousel slide, enabling screen readers to understand which elements are being controlled.

    tabindex="0": Makes the element reachable via the Tab key. A value of tabindex="-1" removes the element from the tab order.

    aria-hidden="true": Hides the content from screen readers when it is not currently visible. This attribute is toggled as the user navigates between slides.

    aria-current="true": Identifies the current item within a set of related elements (like the current slide or the current dot). It is toggled as the active slide changes.

Order of Elements: For better accessibility, follow this structure:

  1. Container (main wrapper for the entire carousel)

  2. Dot Navigation (dot controls for navigation)

  3. Carousel Items (the slides)

  4. Paddle Navigation (previous/next buttons)

Text Zoom

it's important to ensure the layout adapts to text zooming. According to WCAG 2.1 guidelines, users should be able to zoom text up to 200% (or 4x) without any loss of content or functionality.

  1. It’s crucial to avoid hardcoded pixel-based measurements like px for padding, margins, and font sizes. Instead, we should use relative units like em, rem, or %. This ensures that the layout scales proportionally when the user increases text size using the browser’s zoom;

  2. Avoid Fixed Heights: Setting a fixed height (like height: 200px;) can cause content to overflow when text is zoomed. Instead, allow the height to adjust automatically using auto or min-height to create a flexible layout. If a dynamic height is unavoidable, you can handle it using JavaScript. When the text inside the carousel changes — such as when zooming or switching to a different language — the container's height may need to adjust. This is where ResizeObserver comes in. It listens for changes in the size of elements and allows you to update the carousel's height in response dynamically.

4️⃣ Conclusion

I hope these tips were helpful, especially in tackling carousel accessibility in Safari. Building a smooth, accessible, and cross-browser-compatible carousel can be challenging, but with the right techniques, it’s definitely achievable.

If you've discovered any other solutions or techniques that worked for you, I’d love to hear about them! Feel free to share them with me on my social media channels — let's learn and improve together. 🚀