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
Debit card content
Fake test link ACredit card content
Fake test link BSavings card content
Fake test link CThe code
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
/* 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
- Controls appear before the content in the keychain. The dot indicators are also buttons, and appear before the left and right buttons. This order, I'm led to believe, is the best for screen-readers, even though it breaks focus order guidance.
- Semantic HTML: The HTML structure utilises semantic elements such as <div>, <button>, and appropriate ARIA roles (role="region", role="group", etc.), which help convey the structure and purpose of the content to assistive technologies. This supports WCAG Success Criteria 1.3.1 (Info and Relationships) and 4.1.2 (Name, Role, Value).
- ARIA attributes: ARIA attributes such as aria-label, aria-roledescription, aria-controls, and aria-labelledby are used to provide additional context and association between elements, enhancing accessibility for screen reader users. This aligns with WCAG Success Criteria 1.3.1 (Info and Relationships) and 4.1.2 (Name, Role, Value).
- Hidden content: Content that is not currently visible (e.g., hidden slides and content sections) is marked as hidden using the hidden attribute. This ensures that screen reader users are not presented with irrelevant or redundant information, supporting WCAG Success Criteria 1.3.1 (Info and Relationships) and 4.1.2 (Name, Role, Value).
- Live region: A live region (live-region) with aria-live="polite" is included to announce dynamic updates, such as changes in slide content. This ensures that screen reader users are informed of important changes without requiring manual interaction, supporting WCAG Success Criteria 4.1.3 (Status Messages) and 4.1.4 (Live Regions).
- Keyboard accessibility supporting WCAG Success Criteria 2.1.1 (Keyboard) and 2.1.2 (No Keyboard Trap).