question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add accessible Carousel component

See original GitHub issue

WAI-ARIA Authoring Practices has a section about building accessible carousel components:

A carousel presents a set of items, referred to as slides, by sequentially displaying a subset of one or more slides. Typically, one slide is displayed at a time, and users can activate a next or previous slide control that hides the current slide and “rotates” the next or previous slide into view. In some implementations, rotation automatically starts when the page loads, and it may also automatically stop once all the slides have been displayed. While a slide may contain any type of content, image carousels where each slide contains nothing more than a single image are common.

I think there is a great opportunity to build a composable component by following Reach UI’s Philosophy. As I’ve observed, there are several layers of abstraction:

  • Carousel – Similar to Reach UI’s Dialog, it would just wrap the 2 children below
    • CarouselControls – Contains Rotation Control, Next Slide Control, Previous Slide Control and Slide Picker Controls
    • CarouselRotator – Touch-controllable scroll container with a hidden scrollbar
      • CarouselSlide – Holds content to be presented

I’ve already started building the foundation of some components above, mostly by following a CSS-Tricks article about Scroll Snap:

CarouselRotator

import { Flex, FlexProps } from '@chakra-ui/core';
import React from 'react';
import { MarginProps, ResponsiveValue } from 'styled-system';
import CarouselSlide from './CarouselSlide';

// TODO: https://www.w3.org/TR/wai-aria-practices-1.1/#grouped-carousel-elements

function negateResponsiveValue<T>(value: ResponsiveValue<T>) {
  if (value == null) return value;
  if (typeof value === 'number') return -value;
  if (typeof value === 'string') return `-${value}`;
  if (Array.isArray(value)) return value.map(v => (v != null ? `${-v}` : v));
  return Object.fromEntries(
    Object.entries(value).map(([k, v]) => [k, v != null ? `${-v}` : v]),
  );
}

export interface CarouselProps extends FlexProps {
  children: React.ReactComponentElement<typeof CarouselSlide>[];
  slideIndex?: number;
  spacing?: MarginProps['margin'];
  spacingX?: MarginProps['mx'];
  spacingY?: MarginProps['my'];
}

export default function Carousel({
  children,
  slideIndex = 0,
  spacing,
  spacingX,
  spacingY,
  ...restProps
}: CarouselProps) {
  return (
    <Flex
      as="section"
      aria-roledescription="carousel"
      aria-live="polite" // The carousel is NOT automatically rotating
      my={negateResponsiveValue(spacingY != null ? spacingY : spacing)}
      overflow="auto"
      css={{
        scrollSnapType: 'x mandatory',
        // TODO: Leave vendor prefixing to the underlying library
        '::-webkit-scrollbar': { width: 0 },
        '-msOverflowStyle': 'none',
        scrollbarWidth: 'none',
      }}
      {...restProps}
    >
      {React.Children.map(children, (child, i) =>
        React.cloneElement(child, {
          inert: i !== slideIndex ? '' : undefined,
          px: spacingX != null ? spacingX : spacing,
          py: spacingY != null ? spacingY : spacing,
        }),
      )}
    </Flex>
  );
}

There is still a lot of work to do, e.g. adding support for automatic rotating by setting aria-live to off.

CarouselSlide

import { Box, BoxProps } from '@chakra-ui/core';
import React from 'react';

// TODO: Follow the status of https://github.com/WICG/inert and remove polyfill
import 'wicg-inert';

export default function CarouselSlide({
  children,
  inert,
  ...restProps
}: BoxProps) {
  return (
    <Box
      role="group"
      aria-roledescription="slide"
      flex="0 0 100%"
      css={{ scrollSnapAlign: 'center' }}
      {...restProps}
    >
      {/* TODO: Remove extra `div` once `shouldForwardProp` of `Box` supports `inert` */}
      <div inert={inert}>{children}</div>
    </Box>
  );
}

The inert attribute is required to disable tab navigation to undisplayed slides.

declarations.d.ts

Unfortunately, TypeScript and React don’t support the inert attribute yet, thus, it cannot be specified as a boolean, but an empty string: ''.

declare module 'react' {
  interface DOMAttributes<T> {
    inert?: '' | undefined;
  }
}

declare global {
  namespace JSX {
    interface IntrinsicAttributes {
      inert?: '' | undefined;
    }
  }
}

export {};

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:20
  • Comments:57 (19 by maintainers)

github_iconTop GitHub Comments

42reactions
MA-Ahmadcommented, Mar 20, 2021

Any update on Carousel component?

35reactions
kripodcommented, Oct 24, 2019

Finally, I’m ready with the first version of the Carousel, implementing the basic variant outlined in WAI-ARIA Authoring Practices 1.1. Unfortunately, I wasn’t able to set up a CodeSandbox, but the code is available as a part of a project I’m working on.

❗ Live demo

A basic mobile-friendly demo is available from here.

✨ Progressive enhancement

The components are made with older browsers in mind, providing graceful fallbacks in legacy environments. The IntersectionObserver polyfill is loaded conditionally through a dynamic import, evading network overhead for users of evergreen browsers.

🚀Usage examples

Basic

function Component() {
  return (
        <Carousel isInfinite autoPlay maxWidth="xl" mx="auto">
      <Image
        alt="Aerial photography of lake viewing mountain under orange skies"
        src="https://images.unsplash.com/photo-1569302911021-297d2362e3d3?w=800&q=80"
      />
      <Image
        alt="Empty road near mountain"
        src="https://images.unsplash.com/photo-1569250814530-1e923fd61bc6?w=800&q=80"
      />
      <Image
        alt="Person standing near waterfalls"
        src="https://images.unsplash.com/photo-1569099377939-569bbac3c4df?w=800&q=80"
      />
    </Carousel>
  );
}

Advanced

Please see the implementation of Carousel for further details.

📖 Reference

As seen in the advanced usage guide above, the implementation consists of:

Following the philosophy of Reach UI, Carousel serves as a convenient wrapper around the CarouselContainer component, providing all the children out of the box. Most users will not have to know anything else about the details below.

CarouselContainer serves as a provider for establishing CarouselContext, and returns a Box with aria-roledescription="carousel". The carousel’s state can be accessed through a publicly available hook called useCarouselControls, offering high-level data and methods:

  • isInfinite
  • isPlaying, togglePlaying()
  • activeIndex, totalCount, goTo(index), jump(delta)

With those, custom components can be built conveniently. For instance, CarouselStepIconButton was built relying solely upon the aforementioned utilities.

CarouselOverlay provides control buttons absolutely positioned on the stacking context of CarouselContainer. Its descendants can be overridden naturally by providing children.

CarouselRotator is the heart of the pack, where all the magic happens. It manages the state of slides and handles user interactions like swiping or focusing with tab. In the future, slides could be virtualized or lazy-loaded for additional performance benefits.

Each slide is wrapped inside a CarouselSlide, and shall be labeled through the aria-label or aria-labelledby prop of carousel children.

Please let me know about any questions or concerns about the implementation, I’m all open for ideas and observations!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Carousels Tutorial | Web Accessibility Initiative (WAI) - W3C
Implement an accessible carousel widget by providing a robust structure and ... Functionality: Add functionality to display and announce carousel items.
Read more >
How to build a more accessible carousel or slider
Use role="group" and a numbered aria-label (like aria-label="slide 1 of 8" ) on the wrapper of each individual slide so screen reader users...
Read more >
Carousels | Accessibility Guidelines
In terms of accessibility, carousels are not ideal and pose some issues. To a visually impaired person, they are rapidly changing images which...
Read more >
Smart.Carousel - Accessibility
Smart HTML elements framework offers full accessibility support for it's web components. Carousel provides a built-in compliance with WAI-ARIA specification.
Read more >
If you must use a carousel, make it accessible - Alison Walden
This article is part of an ongoing series of accessibility tips for experience designers. It discusses how to design a carousel or slider...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found