Accessibility

Simplified WCAG guidance

WCAG 2.2 – Levels A and AA only, with dev notes.

Accessible Carousel (non-rotating)

Visual is kept minimal, enough to see what's going on. This example concentrates on the functionality of the buttons and sections, and the DOM order layout, with the aim to present a robust, understandable, and operable interface to assistive technologies such as screen-readers.

The currently selected carousel slide dictates the content which appears beneath, more like a tab control but without needing arrow keys to navigate.

Example

Fake test link 1
Fake test link A

The code

HTML
HTML
<section 
      role="region"
      aria-roledescription="carousel"
      aria-label="Select card">

  <div class="slide_and_control_wrap">

    <div class="control_wrap"
          role="group"
          aria-label="Carousel controls">

      <button class="btn-dot"
            aria-current="true"
            aria-expanded="true"
            aria-controls="slide_1 content_1"
            aria-label="Debit card (1 of 3)"
            type="button"></button>
      
      <button class="btn-dot" aria-controls="slide_2 content_2" aria-label="Credit card (2 of 3)" type="button"></button>
      
      <button class="btn-dot" aria-controls="slide_3 content_3" aria-label="Savings card (3 of 3)" type="button"></button>

      <button class="btn-previous"
            aria-label="Previous slide"
            type="button"></button>
      <button class="btn-next" aria-label="Next slide" type="button"></button>

    </div>

    <div class="slide_wrap">

      <div id="slide_1"
            role="group"
            aria-roledescription="slide"
            aria-labelledby="carousel-slide-1__heading"
            id="carousel-slide-1">
        <h2 id="carousel-slide-1__heading">Debit card slide</h2>
        <a href>Fake test link 1</a>
        <!-- Further slide contents -->
      </div>

      <div id="slide_2" hidden role="group" aria-roledescription="slide" aria-labelledby="carousel-slide-2__heading" id="carousel-slide-2">
        <h2 id="carousel-slide-2__heading">Credit card slide</h2>
        <a href>Fake test link 2</a>
        <!-- Further slide contents -->
      </div>

      <div id="slide_3" hidden role="group" aria-roledescription="slide" aria-labelledby="carousel-slide-3__heading" id="carousel-slide-3">
        <h2 id="carousel-slide-3__heading">Savings card slide</h2>
        <a href>Fake test link 3</a>
        <!-- Further slide contents -->
      </div>

    </div> <!-- /.slide_wrap -->

  </div> <!-- /.slide_and_control_wrap -->

  <div class="content_wrap">

    <div id="content_1"
          role="group"
          aria-roledescription="content"
          aria-labelledby="carousel-content-1__heading"
          id="carousel-slide-1">
      <h2 id="carousel-content-1__heading">Debit card content</h2>
      <a href>Fake test link A</a>
      <!-- Further html contents -->
    </div>

    <div id="content_2" hidden role="group" aria-roledescription="content" aria-labelledby="carousel-content-2__heading" id="carousel-slide-1">
      <h2 id="carousel-content-2__heading">Credit card content</h2>
      <a href>Fake test link B</a>
      <!-- Further html contents -->
    </div>

    <div id="content_3" hidden role="group" aria-roledescription="content" aria-labelledby="carousel-content-3__heading" id="carousel-slide-1">
      <h2 id="carousel-content-3__heading">Savings card content</h2>
      <a href>Fake test link C</a>
      <!-- Further html contents -->
    </div>

  </div> <!-- /.content_wrap -->
  
  <div class="sr-only"
        id="live-region"
        aria-live="polite"></div>
  
</section>
CSS
CSS
/* Visually hide, screen-reader only content */
.sr-only {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

.slide_and_control_wrap {
  position: relative;
  display: flex;
  flex-direction: column-reverse;
  gap: 4px;
}

.control_wrap {
  width: max-content;
  display: flex;
  gap: 16px;
  margin: 0 auto;
  justify-content: center;
}

.slide_wrap {
  position: relative;
  margin: 0 auto;
  width: 100%;
  text-align: center;
}

.content_wrap {
  background-color: #eef;
  margin: 1rem auto 0;
}


[aria-roledescription=carousel] {
  background-color: #fff;
  padding: 16px;
}

[aria-roledescription=slide] {
  background-color: #eef;
  padding: 16px;
  min-height: 250px;
}

[aria-roledescription=content] {
  background-color: #fef;
  padding: 16px;
  min-height: 300px;
}

/* Buttons */

.control_wrap > :where(button) {
  display: block;
  font-size: 1.5em;
  border: 1px solid var(--btn-border, #8A8A8A);
  padding: 0;
  cursor: pointer;
  
}
button::after {
  display: flex;
  justify-content: center;
  align-items: center;
  min-width: 24px;
  min-height: 24px;
  aspect-ratio: 1 / 1;
}

.btn-dot {
  background-color: transparent;
  border-color: transparent;
  border-radius: 100%;
  color: #767676;
}
.btn-dot::after {
  content: "○";
}
.btn-dot[aria-current=true]::after {
  content: "●";
}

.btn-previous,
.btn-next {
  border-radius: 4px;
  color: #666;
  position: absolute;
  top: 50%;
  transform: translate3d(0, calc(-50% - 12px - 2px), 0);
  z-index: 1;
  height: 64px;
}
.btn-previous {
  left: 4px;
}
.btn-next {
  right: 4px;
}
@media (min-width:40em) {
  .btn-previous {
    left: 16px;
  }
  .btn-next {
    right: 16px;
  }
}
.btn-previous::after {
  content: "⇦";
}
.btn-next::after {
  content: "⇨";
}


:focus-visible {
  outline: 2px solid blue;
  outline-offset: 2px
}


.slide_wrap div:first-of-type {
  background:yellow;
}
.slide_wrap div:last-of-type {
  background:cyan;
}
.content_wrap div:first-of-type {
  background: #ff09;
}
.content_wrap div:last-of-type {
  background: #00ffff4f;
}

Features