UI Components Medium
ARIA Carousel
Accessible carousel with play/pause controls, slide announcements and full keyboard navigation following ARIA carousel pattern.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #e4e4e7;
line-height: 1.6;
min-height: 100vh;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.demo {
max-width: 720px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
.demo-title {
font-size: 1.75rem;
font-weight: 700;
color: #fafafa;
letter-spacing: -0.02em;
}
.demo-sub {
color: #a1a1aa;
margin-top: 0.25rem;
font-size: 0.95rem;
}
/* Carousel */
.carousel-wrapper {
margin-top: 2rem;
}
.carousel {
position: relative;
background: #111113;
border: 1px solid #27272a;
border-radius: 14px;
overflow: hidden;
}
.carousel-viewport {
overflow: hidden;
}
.carousel-track {
display: flex;
transition: transform 0.4s ease;
}
.carousel-slide {
min-width: 100%;
flex-shrink: 0;
}
/* Slide Content */
.slide-content {
padding: 3rem 2.5rem;
min-height: 280px;
display: flex;
flex-direction: column;
justify-content: center;
}
.slide-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 0.25rem 0.65rem;
border-radius: 100px;
width: fit-content;
margin-bottom: 1rem;
}
.slide-title {
font-size: 1.5rem;
font-weight: 700;
color: #fafafa;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
}
.slide-desc {
font-size: 0.925rem;
color: #a1a1aa;
max-width: 480px;
line-height: 1.7;
}
/* Slide Color Themes */
.slide-content--blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(59, 130, 246, 0.02));
}
.slide-content--blue .slide-badge {
background: rgba(59, 130, 246, 0.15);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.25);
}
.slide-content--purple {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(139, 92, 246, 0.02));
}
.slide-content--purple .slide-badge {
background: rgba(139, 92, 246, 0.15);
color: #a78bfa;
border: 1px solid rgba(139, 92, 246, 0.25);
}
.slide-content--green {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.08), rgba(34, 197, 94, 0.02));
}
.slide-content--green .slide-badge {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.25);
}
.slide-content--amber {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08), rgba(245, 158, 11, 0.02));
}
.slide-content--amber .slide-badge {
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.25);
}
.slide-content--rose {
background: linear-gradient(135deg, rgba(244, 63, 94, 0.08), rgba(244, 63, 94, 0.02));
}
.slide-content--rose .slide-badge {
background: rgba(244, 63, 94, 0.15);
color: #fb7185;
border: 1px solid rgba(244, 63, 94, 0.25);
}
/* Controls */
.carousel-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem 1.5rem;
border-top: 1px solid #1e1e22;
background: #0e0e10;
}
.carousel-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: #18181b;
border: 1px solid #27272a;
border-radius: 8px;
color: #a1a1aa;
cursor: pointer;
transition: all 0.15s;
}
.carousel-btn:hover {
background: #27272a;
color: #fafafa;
}
.carousel-btn:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Dots */
.carousel-dots {
display: flex;
gap: 0.4rem;
}
.carousel-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #27272a;
border: none;
cursor: pointer;
transition: all 0.2s;
padding: 0;
}
.carousel-dot:hover {
background: #52525b;
}
.carousel-dot--active {
background: #3b82f6;
width: 24px;
border-radius: 100px;
}
.carousel-dot:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Play / Pause */
.carousel-playpause {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #d4d4d8;
font-size: 0.75rem;
font-family: inherit;
cursor: pointer;
transition: all 0.15s;
z-index: 10;
}
.carousel-playpause:hover {
background: rgba(0, 0, 0, 0.7);
color: #fafafa;
}
.carousel-playpause:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.carousel-playpause[data-playing="false"] .icon-pause {
display: none;
}
.carousel-playpause[data-playing="false"] .icon-play {
display: block;
}
.carousel-playpause .icon-play[hidden] {
display: none;
}
@media (max-width: 640px) {
.demo {
padding: 1.5rem 0.75rem;
}
.demo-title {
font-size: 1.35rem;
}
.demo-sub {
font-size: 0.85rem;
}
.carousel-wrapper {
margin-top: 1.25rem;
}
.slide-content {
padding: 1.5rem 1.25rem;
min-height: 200px;
}
.slide-badge {
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
margin-bottom: 0.75rem;
}
.slide-title {
font-size: 1.15rem;
margin-bottom: 0.5rem;
}
.slide-desc {
font-size: 0.82rem;
line-height: 1.6;
}
.carousel-controls {
padding: 0.75rem 1rem;
gap: 0.75rem;
}
.carousel-btn {
width: 32px;
height: 32px;
}
.carousel-playpause {
top: 0.75rem;
right: 0.75rem;
padding: 0.3rem 0.6rem;
font-size: 0.7rem;
}
.carousel-dot {
width: 8px;
height: 8px;
}
.carousel-dot--active {
width: 20px;
}
}
@media (max-width: 380px) {
.slide-content {
padding: 1.25rem 1rem;
min-height: 180px;
}
.slide-title {
font-size: 1.05rem;
}
.slide-desc {
font-size: 0.78rem;
}
}(() => {
const track = document.getElementById("carousel-track");
const slides = Array.from(track.children);
const dots = Array.from(document.querySelectorAll(".carousel-dot"));
const prevBtn = document.getElementById("prev-btn");
const nextBtn = document.getElementById("next-btn");
const playPauseBtn = document.getElementById("playpause-btn");
const liveRegion = document.getElementById("carousel-live");
const carousel = document.querySelector(".carousel");
const TOTAL = slides.length;
const INTERVAL = 5000;
let currentIndex = 0;
let isPlaying = true;
let autoplayTimer = null;
function goToSlide(index) {
currentIndex = ((index % TOTAL) + TOTAL) % TOTAL;
// Move track
track.style.transform = `translateX(-${currentIndex * 100}%)`;
// Update slides
slides.forEach((slide, i) => {
slide.classList.toggle("carousel-slide--active", i === currentIndex);
});
// Update dots
dots.forEach((dot, i) => {
const isActive = i === currentIndex;
dot.classList.toggle("carousel-dot--active", isActive);
dot.setAttribute("aria-selected", isActive ? "true" : "false");
});
// Announce to screen readers
announce(`Slide ${currentIndex + 1} of ${TOTAL}`);
}
function nextSlide() {
goToSlide(currentIndex + 1);
}
function prevSlide() {
goToSlide(currentIndex - 1);
}
function announce(text) {
liveRegion.textContent = "";
// Force the live region to re-announce by clearing then setting
requestAnimationFrame(() => {
liveRegion.textContent = text;
});
}
// Auto-play
function startAutoplay() {
stopAutoplay();
autoplayTimer = setInterval(nextSlide, INTERVAL);
isPlaying = true;
updatePlayPauseUI();
}
function stopAutoplay() {
if (autoplayTimer) {
clearInterval(autoplayTimer);
autoplayTimer = null;
}
isPlaying = false;
updatePlayPauseUI();
}
function toggleAutoplay() {
if (isPlaying) {
stopAutoplay();
announce("Auto-play paused");
} else {
startAutoplay();
announce("Auto-play resumed");
}
}
function updatePlayPauseUI() {
const pauseIcon = playPauseBtn.querySelector(".icon-pause");
const playIcon = playPauseBtn.querySelector(".icon-play");
const text = playPauseBtn.querySelector(".playpause-text");
if (isPlaying) {
pauseIcon.hidden = false;
playIcon.hidden = true;
text.textContent = "Pause";
playPauseBtn.setAttribute("aria-label", "Pause auto-play");
playPauseBtn.removeAttribute("data-playing");
} else {
pauseIcon.hidden = true;
playIcon.hidden = false;
text.textContent = "Play";
playPauseBtn.setAttribute("aria-label", "Start auto-play");
playPauseBtn.setAttribute("data-playing", "false");
}
}
// Event listeners
prevBtn.addEventListener("click", () => {
prevSlide();
if (isPlaying) startAutoplay(); // Reset timer
});
nextBtn.addEventListener("click", () => {
nextSlide();
if (isPlaying) startAutoplay();
});
playPauseBtn.addEventListener("click", toggleAutoplay);
dots.forEach((dot) => {
dot.addEventListener("click", () => {
const index = parseInt(dot.dataset.slide, 10);
goToSlide(index);
if (isPlaying) startAutoplay();
});
});
// Keyboard: arrows for prev/next
carousel.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
e.preventDefault();
prevSlide();
if (isPlaying) startAutoplay();
} else if (e.key === "ArrowRight") {
e.preventDefault();
nextSlide();
if (isPlaying) startAutoplay();
}
});
// Pause on hover and focus within
carousel.addEventListener("mouseenter", () => {
if (isPlaying) {
clearInterval(autoplayTimer);
autoplayTimer = null;
}
});
carousel.addEventListener("mouseleave", () => {
if (isPlaying && !autoplayTimer) {
autoplayTimer = setInterval(nextSlide, INTERVAL);
}
});
carousel.addEventListener("focusin", () => {
if (isPlaying) {
clearInterval(autoplayTimer);
autoplayTimer = null;
}
});
carousel.addEventListener("focusout", (e) => {
if (isPlaying && !carousel.contains(e.relatedTarget)) {
autoplayTimer = setInterval(nextSlide, INTERVAL);
}
});
// Start
startAutoplay();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ARIA Carousel</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">ARIA Carousel</h1>
<p class="demo-sub">Accessible carousel with auto-play, keyboard navigation, and slide announcements.</p>
<div class="carousel-wrapper">
<section class="carousel"
aria-roledescription="carousel"
aria-label="Featured content">
<!-- Slide Track -->
<div class="carousel-viewport">
<div class="carousel-track" id="carousel-track">
<!-- Slide 1 -->
<div class="carousel-slide carousel-slide--active"
role="group"
aria-roledescription="slide"
aria-label="1 of 5"
id="slide-1">
<div class="slide-content slide-content--blue">
<div class="slide-badge">Design Systems</div>
<h2 class="slide-title">Building Consistent UI at Scale</h2>
<p class="slide-desc">Create reusable components with design tokens, ensuring visual consistency across your entire product suite.</p>
</div>
</div>
<!-- Slide 2 -->
<div class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="2 of 5"
id="slide-2">
<div class="slide-content slide-content--purple">
<div class="slide-badge">Accessibility</div>
<h2 class="slide-title">Inclusive Design Patterns</h2>
<p class="slide-desc">Every component follows WCAG 2.1 AA guidelines with proper keyboard navigation, screen reader support, and focus management.</p>
</div>
</div>
<!-- Slide 3 -->
<div class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="3 of 5"
id="slide-3">
<div class="slide-content slide-content--green">
<div class="slide-badge">Performance</div>
<h2 class="slide-title">Optimized for Speed</h2>
<p class="slide-desc">Zero dependencies, tree-shakeable exports, and CSS-first animations keep your bundle size under control.</p>
</div>
</div>
<!-- Slide 4 -->
<div class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="4 of 5"
id="slide-4">
<div class="slide-content slide-content--amber">
<div class="slide-badge">Developer Experience</div>
<h2 class="slide-title">TypeScript First</h2>
<p class="slide-desc">Full type safety with auto-generated documentation, IntelliSense support, and comprehensive test coverage.</p>
</div>
</div>
<!-- Slide 5 -->
<div class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="5 of 5"
id="slide-5">
<div class="slide-content slide-content--rose">
<div class="slide-badge">Community</div>
<h2 class="slide-title">Open Source & Growing</h2>
<p class="slide-desc">Join a vibrant community of contributors. Submit components, report issues, and help shape the future of accessible UI.</p>
</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="carousel-controls">
<button class="carousel-btn carousel-btn--prev" id="prev-btn" aria-label="Previous slide">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<!-- Dot Indicators -->
<div class="carousel-dots" role="tablist" aria-label="Slide navigation">
<button class="carousel-dot carousel-dot--active" role="tab" aria-selected="true" aria-label="Slide 1" data-slide="0"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Slide 2" data-slide="1"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Slide 3" data-slide="2"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Slide 4" data-slide="3"></button>
<button class="carousel-dot" role="tab" aria-selected="false" aria-label="Slide 5" data-slide="4"></button>
</div>
<button class="carousel-btn carousel-btn--next" id="next-btn" aria-label="Next slide">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
<!-- Play / Pause -->
<button class="carousel-playpause" id="playpause-btn" aria-label="Pause auto-play">
<svg class="icon-pause" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
<svg class="icon-play" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" hidden><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span class="playpause-text">Pause</span>
</button>
<!-- Live Region for announcements -->
<div class="sr-only" aria-live="polite" aria-atomic="true" id="carousel-live"></div>
</section>
</div>
</div>
<script src="script.js"></script>
</body>
</html>An accessible carousel with auto-play, play/pause toggle, previous/next buttons, and dot indicators. Slide changes are announced via a live region, and auto-play pauses on keyboard focus or hover to avoid disorienting screen reader users.