Why Build a Carousel from Scratch?
Image carousels remain one of the most common UI components on the web, found on countless homepages, product pages, and portfolio sites. While libraries exist to add carousel functionality, building one from scratch with vanilla JavaScript gives you complete control over performance, accessibility, and user experience.
The Case for Vanilla JavaScript
Third-party carousel libraries often include unnecessary features that bloat your JavaScript bundle. By building a carousel yourself, you include only what you need. This approach offers several advantages:
- Complete control over styling and behavior - Customize every aspect without fighting library defaults
- Lighter bundle sizes - No external dependencies means faster page loads
- Easier debugging - Your code is entirely under your control
- No dependency risk - Libraries may become outdated, abandoned, or change their licensing
According to Web.dev's carousel best practices, using JavaScript to initiate the loading of carousel content is one of the biggest performance mistakes to avoid. When you build your own carousel, you can ensure content loads efficiently from the start using native HTML markup.
When to Use Carousels
Carousels work best when displaying multiple related items in a limited space. Common use cases include:
- Product showcases - Display multiple products in a compact space
- Testimonial sliders - Share customer success stories elegantly
- Portfolio galleries - Showcase work samples without overwhelming visitors
- Featured content highlights - Draw attention to important announcements
However, carousels require careful implementation. Common pitfalls include confusing navigation patterns, slow performance from unoptimized images, and poor accessibility for screen reader users. Building from scratch helps you avoid these issues by making intentional choices about every behavior.
If you need a carousel that truly enhances your website while maintaining excellent performance and accessibility, building it yourself is the right approach. The initial investment pays dividends in maintainability and user experience. For custom web solutions that prioritize performance and accessibility, explore our web development services to learn how we build tailored components for every project.
HTML Structure and Semantic Markup
The foundation of any accessible carousel is its HTML structure. Using proper semantic markup ensures screen readers and other assistive technologies can interpret your carousel correctly.
The Carousel Container
The outermost element wraps the entire carousel and should have appropriate ARIA attributes to identify it as a significant region:
<div class="carousel"
role="region"
aria-label="Product slideshow"
aria-live="polite">
<!-- Carousel content goes here -->
</div>
The role="region" attribute marks this as a significant section of the page, while aria-label provides a descriptive name. The aria-live="polite" setting tells screen readers to announce slide changes without interrupting the user, as outlined in Chrome's accessible carousel guide.
Complete Slide Structure
Each slide should be contained in a semantic element that clearly identifies its role. Here's the complete structure including navigation controls and indicators:
<div class="carousel" role="region" aria-label="Image slideshow" aria-live="polite">
<div class="carousel-track">
<div class="carousel-slide" aria-hidden="false">
<img src="slide1.jpg" alt="Description of first slide">
</div>
<div class="carousel-slide" aria-hidden="true">
<img src="slide2.jpg" alt="Description of second slide">
</div>
<div class="carousel-slide" aria-hidden="true">
<img src="slide3.jpg" alt="Description of third slide">
</div>
</div>
<!-- Navigation controls -->
<button class="carousel-button prev" aria-label="Previous slide">
<span aria-hidden="true">←</span>
</button>
<button class="carousel-button next" aria-label="Next slide">
<span aria-hidden="true">→</span>
</button>
<!-- Slide indicators -->
<div class="carousel-indicators" role="tablist" aria-label="Slide navigation">
<button class="indicator active" role="tab" aria-selected="true" aria-label="Go to slide 1"></button>
<button class="indicator" role="tab" aria-selected="false" aria-label="Go to slide 2"></button>
<button class="indicator" role="tab" aria-selected="false" aria-label="Go to slide 3"></button>
</div>
</div>
Accessibility Considerations in HTML
Proper ARIA attributes are essential for screen reader users. The aria-hidden attribute on non-visible slides prevents screen readers from announcing content users cannot see. Each navigation control needs descriptive aria-label attributes that clearly communicate its purpose and current context.
The indicators use role="tablist" and role="tab" semantics, following the established pattern for tab interfaces that many screen reader users are already familiar with. The aria-selected attribute indicates which indicator corresponds to the currently visible slide.
Using proper semantic markup from the start ensures your carousel works with assistive technologies without requiring complex workarounds later in development. Building accessible components like this is a core part of modern web development practices that prioritize all users.
CSS Styling and Transitions
With the HTML structure in place, CSS handles the visual presentation and animations. Modern CSS features make it easier than ever to create smooth, performant transitions.
CSS Scroll Snap
The CSS Scroll Snap module provides a native way to create carousel-like behavior without requiring JavaScript for the snapping effect. This approach leverages the browser's rendering engine for optimal performance:
.carousel-track {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
}
.carousel-track::-webkit-scrollbar {
display: none;
}
.carousel-slide {
flex: 0 0 100%;
scroll-snap-align: center;
}
.carousel-slide img {
width: 100%;
height: auto;
object-fit: cover;
}
This approach ensures that slides snap to the center position when scrolling stops, providing a polished user experience. The scroll-behavior: smooth property adds smooth animation when using JavaScript to navigate programmatically, as recommended in Web.dev's carousel best practices.
Transitions and Animations
For more control over transitions beyond what Scroll Snap provides, use CSS transforms and opacity:
.carousel-slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
opacity: 0;
transform: translateX(100%);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.carousel-slide.active {
opacity: 1;
transform: translateX(0);
}
.carousel-slide.exit {
transform: translateX(-100%);
}
Using transform and opacity is crucial because these properties can be animated efficiently by the browser without triggering layout recalculations, ensuring smooth 60fps animations.
Responsive Design
Carousels must work across all screen sizes. Use CSS media queries to adapt the layout for different viewports:
@media (max-width: 768px) {
.carousel-slide {
flex: 0 0 100%;
}
.carousel-button {
width: 44px;
height: 44px;
font-size: 1.25rem;
}
.indicator {
width: 12px;
height: 12px;
}
}
@media (min-width: 769px) {
.carousel-slide {
flex: 0 0 50%; /* Show partial next slide on larger screens */
}
}
/* High-resolution displays */
@media (min-resolution: 2dppx) {
.carousel-slide img {
object-fit: cover;
}
}
Note the 44px minimum for touch targets on mobile devices, following accessibility guidelines for interactive elements.
1/* Carousel track styles */2.carousel-track {3 display: flex;4 overflow-x: auto;5 scroll-snap-type: x mandatory;6 scroll-behavior: smooth;7 scrollbar-width: none;8}9 10.carousel-track::-webkit-scrollbar {11 display: none;12}13 14/* Slide styles */15.carousel-slide {16 flex: 0 0 100%;17 scroll-snap-align: center;18}19 20.carousel-slide img {21 width: 100%;22 height: auto;23 object-fit: cover;24}25 26/* Navigation button styles */27.carousel-button {28 position: absolute;29 top: 50%;30 transform: translateY(-50%);31 width: 40px;32 height: 40px;33 border: none;34 border-radius: 50%;35 background: rgba(255, 255, 255, 0.9);36 cursor: pointer;37 z-index: 10;38 transition: background 0.2s ease;39}40 41.carousel-button:hover {42 background: white;43}44 45.carousel-button.prev {46 left: 10px;47}48 49.carousel-button.next {50 right: 10px;51}52 53/* Indicator styles */54.carousel-indicators {55 display: flex;56 justify-content: center;57 gap: 8px;58 padding: 10px;59}60 61.indicator {62 width: 10px;63 height: 10px;64 border-radius: 50%;65 border: 2px solid #333;66 background: transparent;67 cursor: pointer;68 padding: 0;69}70 71.indicator.active {72 background: #333;73}74 75/* Active state */76.carousel-slide.active {77 opacity: 1;78 transform: translateX(0);79}80 81/* Exit animation */82.carousel-slide.exit {83 transform: translateX(-100%);84}JavaScript Functionality
With HTML and CSS providing structure and styling, JavaScript adds interactivity. We'll implement navigation, auto-play functionality, touch support, and keyboard accessibility using a well-organized class-based approach.
Why Use a Class-Based Approach
Encapsulating carousel functionality in a class provides several benefits: clear state management, reusable initialization, easy debugging through a single entry point, and the ability to create multiple independent carousel instances on the same page. The class pattern keeps related functionality together while maintaining clean separation from other code.
The constructor initializes all required DOM references and sets up the initial state. Event listeners are bound in the init() method, keeping the constructor clean and focused on setup. All carousel logic is contained within the class, preventing global namespace pollution and making testing straightforward.
Initialization
Initialize the carousel when the DOM is ready, creating instances for each carousel on your page:
document.addEventListener('DOMContentLoaded', () => {
const carouselContainer = document.querySelector('.carousel');
if (carouselContainer) {
new Carousel(carouselContainer);
}
// Support multiple carousels on one page
document.querySelectorAll('.carousel').forEach(container => {
new Carousel(container);
});
});
This pattern allows multiple carousels to exist on the same page without interfering with each other, as each instance manages its own state independently. Writing clean, modular JavaScript like this is essential for maintainable web development projects.
1class Carousel {2 constructor(container) {3 this.container = container;4 this.track = container.querySelector('.carousel-track');5 this.slides = container.querySelectorAll('.carousel-slide');6 this.prevButton = container.querySelector('.carousel-button.prev');7 this.nextButton = container.querySelector('.carousel-button.next');8 this.indicators = container.querySelectorAll('.indicator');9 10 this.currentIndex = 0;11 this.slideCount = this.slides.length;12 this.autoPlayInterval = null;13 14 this.init();15 }16 17 init() {18 // Bind navigation button events19 this.prevButton.addEventListener('click', () => this.prev());20 this.nextButton.addEventListener('click', () => this.next());21 22 // Bind indicator events23 this.indicators.forEach((indicator, index) => {24 indicator.addEventListener('click', () => this.goToSlide(index));25 });26 27 // Keyboard navigation28 this.container.addEventListener('keydown', (e) => this.handleKeyboard(e));29 30 // Pause auto-play on interaction31 this.container.addEventListener('mouseenter', () => this.pauseAutoPlay());32 this.container.addEventListener('mouseleave', () => this.startAutoPlay());33 34 // Touch support35 this.setupTouchSupport();36 37 // Set initial state38 this.updateSlide();39 this.startAutoPlay();40 }41 42 goToSlide(index) {43 // Handle circular navigation44 if (index < 0) {45 this.currentIndex = this.slideCount - 1;46 } else if (index >= this.slideCount) {47 this.currentIndex = 0;48 } else {49 this.currentIndex = index;50 }51 this.updateSlide();52 }53 54 next() {55 this.goToSlide(this.currentIndex + 1);56 }57 58 prev() {59 this.goToSlide(this.currentIndex - 1);60 }61 62 updateSlide() {63 // Update slide visibility and ARIA states64 this.slides.forEach((slide, index) => {65 slide.classList.toggle('active', index === this.currentIndex);66 slide.setAttribute('aria-hidden', index !== this.currentIndex);67 });68 69 // Update indicators70 this.indicators.forEach((indicator, index) => {71 indicator.classList.toggle('active', index === this.currentIndex);72 indicator.setAttribute('aria-selected', index === this.currentIndex);73 });74 }75 76 startAutoPlay() {77 // Advance every 5 seconds78 this.autoPlayInterval = setInterval(() => this.next(), 5000);79 }80 81 pauseAutoPlay() {82 clearInterval(this.autoPlayInterval);83 }84 85 handleKeyboard(e) {86 switch(e.key) {87 case 'ArrowLeft':88 e.preventDefault();89 this.prev();90 break;91 case 'ArrowRight':92 e.preventDefault();93 this.next();94 break;95 case 'Home':96 e.preventDefault();97 this.goToSlide(0);98 break;99 case 'End':100 e.preventDefault();101 this.goToSlide(this.slideCount - 1);102 break;103 }104 }105 106 setupTouchSupport() {107 let startX = 0;108 let isDragging = false;109 110 this.track.addEventListener('touchstart', (e) => {111 startX = e.touches[0].clientX;112 isDragging = true;113 });114 115 this.track.addEventListener('touchmove', (e) => {116 if (!isDragging) return;117 const currentX = e.touches[0].clientX;118 const diff = startX - currentX;119 120 // 50px threshold to distinguish swipe from tap121 if (Math.abs(diff) > 50) {122 if (diff > 0) this.next();123 else this.prev();124 isDragging = false;125 }126 });127 128 this.track.addEventListener('touchend', () => {129 isDragging = false;130 });131 }132}Modern CSS Approaches with Overflow 5
CSS Overflow Module Level 5 introduces powerful new features specifically designed for creating accessible carousels without requiring extensive JavaScript for interactivity management.
Scroll-State Container Queries
The new scroll-state() container query and interactivity property allow you to control which content is interactive based on scroll position, as detailed in Chrome's accessible carousel guide. This significantly simplifies accessibility implementation:
.carousel-track {
container-type: scroll-state;
}
.carousel-slide {
/* Make all slide content inert by default */
interactivity: inert;
}
/* Make content interactive only when snapped inline */
@container scroll-state(snapped: inline) {
.carousel-slide[style*="--snapped: inline"] > * {
interactivity: auto;
}
}
How Interactivity: Inert Works
The interactivity: inert property prevents users from interacting with off-screen content in carousels. This is crucial for accessibility because it:
- Prevents keyboard focus from reaching off-screen slides
- Blocks click and touch events on hidden content
- Eliminates the need for manual aria-hidden management in JavaScript
- Ensures screen readers only announce visible content
When a slide snaps into view, the container query detects the snap state and applies interactivity: auto to that slide's content, making it fully interactive. This automatic behavior reduces JavaScript complexity while improving accessibility.
Scroll Markers
CSS Overflow 5 also introduces scroll markers for indicating position without additional markup:
.carousel-slide::scroll-marker {
content: counter(scroll-marker-group, decimal);
/* Styling for the marker */
}
.carousel-slide::scroll-marker-group {
counter-reset: scroll-marker-group;
}
These native markers provide a semantic way to show slide position, reducing the need for custom indicator implementations. Keeping up with modern CSS features like these helps developers build more efficient and accessible web interfaces as part of comprehensive web development services.
Performance Optimization
A poorly optimized carousel can significantly impact page performance, especially Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS) metrics that directly affect your Core Web Vitals scores.
Efficient Image Loading
Carousel content should be loaded via HTML markup so the browser can discover it early in the page load process. Using JavaScript to create slides and initiate image loading delays image discovery and negatively impacts LCP, as explained in Web.dev's carousel best practices. Structure your HTML like this:
<div class="carousel-track">
<img src="slide1.jpg" alt="Description" loading="eager">
<img src="slide2.jpg" alt="Description" loading="lazy">
<img src="slide3.jpg" alt="Description" loading="lazy">
</div>
The first image uses loading="eager" to load immediately, while subsequent images use loading="lazy" to defer loading until needed. This balances initial page load speed with smooth carousel operation.
Avoiding Layout Shifts
Layout shifts during slide transitions often occur when animating layout-inducing properties like left, top, width, or marginTop. Use CSS transforms instead, as recommended in Web.dev's carousel best practices, which the browser can animate efficiently:
/* Good: Uses transform for smooth transitions */
.carousel-slide {
transform: translateX(0);
transition: transform 0.3s ease;
}
/* Bad: Layout-inducing property changes cause shifts */
.carousel-slide {
left: 0;
transition: left 0.3s ease;
}
Using transform and opacity for animations prevents the browser from recalculating layout, resulting in smoother animations and better CLS scores. Always reserve space for carousel images using aspect-ratio containers to prevent shifts during image loading.
Image Optimization
Since carousels often contain large images, optimization is crucial for performance:
- Use modern formats - WebP and AVIF provide better compression than JPEG
- Implement responsive images - Use
srcsetandsizesfor appropriate resolutions - Consider lazy loading - Defer off-screen images to reduce initial load
- Use an image CDN - Serve optimized images from edge locations
Performance optimization like this is essential for any modern web development project aiming to deliver exceptional user experiences.
LCP Optimization
Load first slide eagerly with loading="eager", lazy load subsequent slides to prioritize above-the-fold content
CLS Prevention
Use CSS transforms instead of layout-changing properties like left/top; reserve space with aspect-ratio containers
Efficient Animations
Use will-change and transform for 60fps transitions; avoid animating layout properties
Bundle Size
Vanilla JS eliminates library overhead, keeping your JavaScript bundle small and fast
Accessibility Implementation
Building an accessible carousel requires attention to keyboard navigation, screen reader support, and focus management. These considerations ensure all users can effectively use your carousel.
Keyboard Navigation
All carousel controls must be keyboard accessible. Users should be able to navigate between slides using arrow keys and jump to the first or last slide using Home and End keys:
handleKeyboard(e) {
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
this.prev();
break;
case 'ArrowRight':
e.preventDefault();
this.next();
break;
case 'Home':
e.preventDefault();
this.goToSlide(0);
break;
case 'End':
e.preventDefault();
this.goToSlide(this.slideCount - 1);
break;
}
}
Focus Management
When navigating between slides, manage focus appropriately to maintain the user's place in the document. This prevents users from losing their position when slides change:
goToSlide(index) {
const previousSlide = this.slides[this.currentIndex];
this.currentIndex = index;
const newSlide = this.slides[this.currentIndex];
// Update visual state first
this.updateSlide();
// Move focus to the new slide container
newSlide.focus();
}
Consider also moving focus to the first interactive element within a slide if it contains links or buttons, maintaining a logical focus flow for keyboard users.
Screen Reader Announcements
Use aria-live regions to announce slide changes to screen reader users. This can be done by updating a visually hidden element that screen readers monitor:
<div class="carousel" aria-live="polite">
<div class="sr-only" aria-live="assertive" id="slide-announcer">
Showing slide 1 of 3
</div>
<!-- Carousel content -->
</div>
Update the announcer text when slides change so users know their current position in the carousel sequence. Accessibility is a core principle of inclusive web development services that serve all users effectively.
Accessibility Checklist
How do I make carousel buttons accessible?
Use native <button> elements with descriptive aria-label attributes for all navigation controls. Ensure buttons are focusable and have visible focus states.
Should I use auto-play?
Avoid auto-play when possible. If used, provide pause controls and pause on user interaction. Consider respecting reduced motion preferences with matchMedia.
How do I handle images in carousels?
Ensure all images have descriptive alt text that conveys the image's meaning. Use aria-hidden for decorative images that don't add content.
What keyboard shortcuts should I support?
Arrow keys for navigation between slides, Home for first slide, End for last slide, and Tab for moving focus through interactive elements.
Common Pitfalls and How to Avoid Them
Building carousels involves navigating several common challenges. Understanding these pitfalls helps you build a better component from the start.
Auto-Play Considerations
Auto-playing carousels can frustrate users who need more time to read or view content. The content may advance before users finish consuming it, animations can distract users trying to focus, and some users find auto-playing content disorienting. To mitigate these issues:
- Pause on interaction - Always pause auto-play when users hover over the carousel or interact with it
- Provide controls - Include visible pause and play buttons for user control
- Match timing to content - Allow more time for complex slides with detailed content
- Respect preferences - Disable auto-play for users who have requested reduced motion via the prefers-reduced-motion media query
// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
// Disable auto-play or use longer intervals
}
Navigation Visibility
Carousel navigation controls should be prominent and highly visible. Controls that are too subtle or blend into the background frustrate users trying to navigate:
- Make buttons clearly visible with contrasting colors
- Provide clear visual feedback on hover and focus states
- Ensure adequate size for touch targets (minimum 44x44 pixels)
- Don't hide controls inside hover menus that mobile users can't access
Mobile Experience
Mobile users expect swipe gestures for carousel navigation, the same way they interact with native photo apps:
- Implement touch swipe support with appropriate thresholds (50px works well)
- Test on actual devices rather than just browser dev tools
- Ensure smooth performance on slower connections
- Optimize images for mobile data savings
Rapid Interaction Edge Cases
Handle rapid button clicks gracefully to prevent animation glitches:
goToSlide(index) {
// Prevent rapid clicks during transitions
if (this.isTransitioning) return;
this.isTransitioning = true;
// Perform transition
this.performTransition(index);
// Reset flag after animation completes
setTimeout(() => {
this.isTransitioning = false;
}, 500);
}
By anticipating these common issues during development, you create a carousel that provides a better experience for all users across all devices.
Testing Your Carousel
Comprehensive testing ensures your carousel works correctly for all users across different devices and assistive technologies.
Functional Testing
Verify that all navigation methods work correctly through systematic testing:
- Button navigation - Test previous and next buttons on each slide
- Indicator navigation - Click each indicator and verify it goes to the correct slide
- Keyboard navigation - Use arrow keys, Home, and End to navigate
- Touch gestures - Swipe left and right on actual mobile devices
- Auto-play controls - Verify pause on hover and resume on mouse leave
- Edge cases - Rapid clicking, navigating past first/last slide, window resizing
Accessibility Testing
Accessibility testing should combine automated tools with manual testing:
- Automated tools - Run Lighthouse audits to identify common issues
- Screen reader testing - Test with VoiceOver (Mac), NVDA (Windows), and TalkBack (Android)
- Keyboard-only navigation - Navigate the entire carousel using only the keyboard
- ARIA verification - Use browser dev tools to inspect ARIA attribute accuracy
Performance Testing
Measure your carousel's performance impact on the overall page:
- LCP timing - Verify the first slide loads quickly without delays
- CLS verification - Check for layout shifts during transitions
- Network analysis - Monitor image loading and size optimization
- Mobile performance - Test on slower connections and older devices
Browser Compatibility
Test across browsers and devices:
- Chrome, Firefox, Safari, and Edge (latest versions)
- iOS Safari and Android Chrome (mobile browsers)
- Different screen sizes and resolutions
Automated testing tools like Playwright or Cypress can help ensure your carousel continues to work correctly as you make changes to your codebase.
Conclusion
Building an image carousel from scratch with vanilla JavaScript gives you complete control over functionality, performance, and accessibility. While it requires more initial effort than dropping in a library, the result is a lighter, more maintainable component that works exactly as you intended.
Key principles to remember:
- Use semantic HTML for accessibility - Proper ARIA attributes and role designations ensure screen reader compatibility
- Leverage CSS for smooth animations - CSS transforms and transitions provide hardware-accelerated performance
- Implement comprehensive JavaScript functionality - A class-based approach keeps code organized and maintainable
- Optimize for performance - Pay attention to LCP, CLS, and image optimization
- Test thoroughly - Cover functional, accessibility, and performance testing across devices
With these foundations, you can create a carousel that enhances rather than hinders the user experience. The investment in building it right pays dividends in better Core Web Vitals scores, improved accessibility compliance, and a more maintainable codebase.
If you're building custom web solutions and want to ensure every component performs optimally, our web development services can help you create performant, accessible interfaces.