Create an Accessible News Carousel as an Angular Standalone Component

I love carousels! The colorful theme park rides with animals and chariots are often beautifully crafted. Kids can spend hours on them, going round and round. Pure joy!

Can I say the same about carousel UI components? Not so much. I actually dislike most forms of web carousels, especially when they change slides by themselves. Also, most of them are inaccessible nightmares. So, I said to myself: Let's do better!

A carousel at night with bright lights. Figures of a horse and tiger are visible. Photo: © Alexander Nadrilyanski / pexels.com

I've created a news carousel as an Angular standalone component. It's accessible, responsive and has smooth animations. Let's take a look at how it's built and how it works.

Demo: Accessible News Carousel

My demo application displays a list of news items, each with a heading, text snippet and a background image. Users can move from slide to slide using swipe gestures or the navigation buttons.

Design and Accessibility Requirements

There is no native HTML element for web carousels. That's why there are so many different custom implementations. At least, the W3C has defined basic requirements in their “ARIA Authoring Practices Guide”. Their definition of the carousel pattern states:

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.

This abstract definition applies to basic image rotators as well as slide shows with complex content. In general, carousels should fulfill the following requirements:

  1. It should convey its structure to assistive technologies.
  2. It should indicate the currently active slide, both visually and to assistive technologies.
  3. Users should not be forced to use swipe gestures to operate the carousel. It should offer simple controls that the user can click on or operate with the keyboard.
  4. Dynamic content changes of the carousel (e.g. moving to the next slide) should be communicated to screen reader users via status messages.
  5. If the carousel can automatically rotate, it also has a button for stopping and restarting rotation.

I hate moving content that starts automatically. That's why my news carousel doesn't rotate on its own and, therefore, has no pause button.

Content Structure and Semantic Markup

The Carousel Container

The ARIA specification doesn't include a carousel role. Therefore, you need to set the generic role="region" and provide a custom description with aria-roledescription="carousel". An accessible label is provided by the aria-label attribute.

In my NewsCarouselComponent, I use the HostBinding decorator to apply these properties to the carousel component's container:

/** * The aria label for the carousel container. * It should convey to screenreader users what the carousel is about. */ @HostBinding('attr.aria-label') @Input() public carouselLabel!: string; @HostBinding('attr.role') role = 'region'; @HostBinding('attr.aria-roledescription') get carouselDescription() { return this.config.carouselDescription; }

The carousel includes two icon buttons to navigate to the previous or next slide. Accessible labels are provided by the aria-label attribute set on the button tags.

The Slide Container

Each slide container has role="group" and the property aria-roledescription set to “slide”. The slide container's aria-label attribute conveys the slide's number and the total set size (e.g., “2 of 5”).

<div *ngFor="let item of newsItems; let i = index" class="news-item" role="group" [attr.aria-roledescription]="config.slideDescription" [attr.aria-label]="(i + 1) + ' ' + config.slideLabel + ' ' + newsItems.length" > <!-- slide content --> </div>

The disadvantage of custom role descriptions is that they're not automatically translated by screen readers. If you're building a multi-lingual website, you'll need to keep that in mind. My NewsCarouselComponent has a config input property with default values in English that can be overriden.

@Input() public config: NewsCarouselConfig = { carouselDescription: 'carousel', slideDescription: 'slide', slideLabel: 'of', nextButtonLabel: 'Next slide', previousButtonLabel: 'Previous slide', }

Avoid List Markup

A common mistake is to use list markup for the slides. Screen readers announce the number of items in a list, but ignore list items that are hidden. In my component, only the visible slide is also presented to assistive technologies. The other slides are visually and programmatically hidden using the CSS property visibility: hidden.

If I'd implement all slides as <li> elements inside an unordered list (<ul>), then the screen reader would announce the presence of a list with only one item. This would not match the true number of slides and be confusing for the user.

ARIA Live Region

The carousel should communicate dynamic content changes to screen reader users via status messages. At first, I tried to turn the container element for all slides into an ARIA live region. The results were not satisfying. Due to the implementation of the slide transition using the visibility CSS property, the screen reader would announce the new slide several times or announce the wrong slide.

I tried different approaches using properties like aria-busy and aria-relevant. They only ever worked for some browser and screen reader combinations. So, in the end I chose the simple approach of putting a visually hidden ARIA live region at the end of the carousel container. It's a bit redundant, but it works!

<div class="active-slide-live-region" aria-live="polite" > {{config.slideDescription + ' ' + (activeSlideIndex + 1) + ' ' + config.slideLabel + ' ' + newsItems.length + ':'}} {{newsItems[activeSlideIndex].heading}} </div>

Keyboard Operability and Swipe Gestures

I love swiping! When I come across a carousel on a website, I instinctively try to swipe its content. Swiping itself is not evil. But a web carousel that is only operable through swipe gestures is!

Swipe gestures are inaccessible to many people with motor disabilities. They prefer single-pointer alternatives like a clickable button. Also, keyboard users require controls they can tab to and activate with the space or enter key.

This is why I try to make everyone happy! My Angular component implements swipe gestures using the Hammer.js library and also includes buttons to navigate to the previous or next slide.

<div class="news-carousel-controls"> <button type="button" [attr.aria-label]="config.previousButtonLabel" (click)="this.showPreviousSlide()" > <!-- arrow icon --> </button> <button type="button" [attr.aria-label]="config.nextButtonLabel" (click)="this.showNextSlide()" > <!-- arrow icon --> </button> </div>

Slide Transitions with CSS Animations

I wanted to create beautiful transitions between slides with smooth slide in and out animations. Angular provides its own animations module. But I wanted to find a solution that would reduce the build size and be efficient at the same time. Which is why I decided to use CSS animations.

Creating animations with CSS is super simple and elegant. First, you define keyframes for your named animations. Each keyframe describes how the animated element should render at a given time during the animation sequence.

// Keyframes for slide animation from left to right @keyframes slide-in-from-left { 0% { transform: translateX(-100%); } 100% { transform: translateX(0%); } } @keyframes slide-out-to-right { 0% { transform: translateX(0%); visibility: visible; } 100% { transform: translateX(100%); visibility: hidden; } }

Then, you apply these animations to specific slides using the animation CSS property. This shorthand allows you to define a wide range of animation related properties (e.g., name, duration, direction) in a single CSS rule.

app-news-carousel .news-carousel-items.animate-from-left .news-item { &.active { animation: slide-in-from-left 0.5s forwards; } &.moved-out { animation: slide-out-to-right 0.5s forwards; border-left: 0.125rem solid white; } }

In the example above, two HTML elements are animated: The element with the class active slides in from the left while the other element with the class moved-out slides out to the right.

Responsive Design and Custom Styling

Flexible Layout with max-width and Container Queries

An application that uses the carousel component can set the CSS variable --news-carousel-max-width to define a basic width for the carousel container. If no variable is set, then the fallback value of the var() function is used.

app-news-carousel { height: var(--news-carousel-height, 25rem); width: var(--news-carousel-max-width, 50rem); max-width: 100%; }

By setting max-width: 100% we ensure that the containers width is limited by the space available to its parent element. This way, the carousel never uses up more space than the viewport width of the device.

Furthermore, I use CSS Container Queries to adapt the carousel's styling if the container's width is below a certain threshold. This new, powerful feature will soon be supported by all major browsers. I'll take a closer look at it in a separate blog post.

@container news-carousel (max-width: 28rem) { app-news-carousel .news-item .slide-text { bottom: 0.5rem; border-radius: 0.5rem; width: calc(100% - 1rem); p { display: none; } } }

Custom Styling with CSS Variables

CSS variables are great for component libraries. You can define a default styling and allow the user to override specific styles by defining the responding variable. An application using my NewsCarouselComponent can customize the following style properties via CSS variables:

  • --news-carousel-height: Set the height of the carousel.
  • --news-carousel-max-width: Set the max width of the carousel. The carousel's actual width is responsive to its container.
  • --news-carousel-button-background: The background (color) of the previous and next buttons.
  • --news-carousel-button-color: The color of the arrow icon in the previous and next buttons.
  • --carousel-text-background: The background (color) of the slides' text container.
  • --carousel-text-color: The text color of the slides' text container.
  • --news-carousel-slide-focus-color: The color of the slides' focus indicator.

What Else to Say

I want to make one thing clear: My carousel component is only one of several ways to implement an accessible, responsive carousel. Yes, I'm pretty confident that my implementation is awesome. But of course, there's other valid approaches as well.

Web standards, browsers and assistive technologies are always evolving and changing. So, in a few years, or maybe even in a few months, there might be a better solution. I can't wait to see it! 😊

Useful Resources

Posted on