Embla Carousel Implementation Guide
Overview
This document provides a comprehensive technical guide for implementing the FadeGallery component using Embla Carousel v8.6.0 in a Docusaurus project. The implementation includes fade transitions, professional navigation controls, and responsive design patterns.
Project Context
Target Environment: Docusaurus v3.8.1 with React 19.0.0 TypeScript: v5.6.2 with strict mode enabled Component Architecture: CSS Modules with theme integration Accessibility: WCAG 2.1 AA compliance
Phase 1: Dependency Installation
1.1 Package Installation
npm install embla-carousel-react embla-carousel-fade
Packages Added:
embla-carousel-react@8.6.0
: Core React wrapperembla-carousel-fade@8.6.0
: Fade transition pluginembla-carousel@8.6.0
: Core library (peer dependency)embla-carousel-reactive-utils@8.6.0
: Reactive utilities (dependency)
1.2 Package.json Updates
{
"dependencies": {
"embla-carousel-fade": "^8.6.0",
"embla-carousel-react": "^8.6.0"
}
}
Phase 2: Reference Implementation Analysis
2.1 Local Resources Structure
The project includes reference implementations at:
/localResources/embla-carousel/
├── src/
│ ├── js/
│ │ ├── EmblaCarousel.tsx
│ │ ├── EmblaCarouselArrowButtons.tsx
│ │ └── EmblaCarouselDotButton.tsx
│ └── css/
│ └── embla.css
2.2 Key Patterns Extracted
Navigation Hooks Pattern
From EmblaCarouselArrowButtons.tsx
:
const usePrevNextButtons = (
emblaApi: EmblaCarouselType | undefined,
onButtonClick?: (emblaApi: EmblaCarouselType) => void
): UsePrevNextButtonsType => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
// Event handlers and state management
}
Dot Navigation Pattern
From EmblaCarouselDotButton.tsx
:
const useDotButton = (
emblaApi: EmblaCarouselType | undefined,
onButtonClick?: (emblaApi: EmblaCarouselType) => void
): UseDotButtonType => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
// Slide tracking and navigation logic
}
Phase 3: Component Architecture Design
3.1 Directory Structure
src/components/FadeGallery/
├── index.tsx # Main component
└── FadeGallery.module.css # Styles
3.2 TypeScript Interface Design
export type FadeGalleryProps = {
/** Array of public paths to image assets */
images: string[];
/** Optional title displayed above gallery */
title?: string;
/** Auto-play interval in milliseconds */
autoplay?: number;
/** Show navigation dots. Default: true */
showDots?: boolean;
/** Show previous/next buttons. Default: true */
showButtons?: boolean;
};
Phase 4: Custom Hooks Implementation
4.1 Previous/Next Button Hook
Adapted from reference implementation with Docusaurus-specific optimizations:
const usePrevNextButtons = (
emblaApi: EmblaCarouselType | undefined
) => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
const onPrevButtonClick = useCallback(() => {
if (!emblaApi) return;
emblaApi.scrollPrev();
}, [emblaApi]);
const onNextButtonClick = useCallback(() => {
if (!emblaApi) return;
emblaApi.scrollNext();
}, [emblaApi]);
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setPrevBtnDisabled(!emblaApi.canScrollPrev());
setNextBtnDisabled(!emblaApi.canScrollNext());
}, []);
useEffect(() => {
if (!emblaApi) return;
onSelect(emblaApi);
emblaApi.on('reInit', onSelect).on('select', onSelect);
return () => {
emblaApi.off('reInit', onSelect).off('select', onSelect);
};
}, [emblaApi, onSelect]);
return {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick,
};
};
Key Adaptations:
- Removed optional
onButtonClick
parameter for simplicity - Added comprehensive event cleanup in useEffect return
- Used strict TypeScript typing throughout
4.2 Dot Button Hook
const useDotButton = (
emblaApi: EmblaCarouselType | undefined
) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
const onDotButtonClick = useCallback(
(index: number) => {
if (!emblaApi) return;
emblaApi.scrollTo(index);
},
[emblaApi]
);
const onInit = useCallback((emblaApi: EmblaCarouselType) => {
setScrollSnaps(emblaApi.scrollSnapList());
}, []);
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setSelectedIndex(emblaApi.selectedScrollSnap());
}, []);
useEffect(() => {
if (!emblaApi) return;
onInit(emblaApi);
onSelect(emblaApi);
emblaApi.on('reInit', onInit).on('reInit', onSelect).on('select', onSelect);
return () => {
emblaApi.off('reInit', onInit).off('reInit', onSelect).off('select', onSelect);
};
}, [emblaApi, onInit, onSelect]);
return {
selectedIndex,
scrollSnaps,
onDotButtonClick,
};
};
Phase 5: Main Component Implementation
5.1 Carousel Initialization
const [emblaRef, emblaApi] = useEmblaCarousel(
{
align: 'center',
containScroll: false,
loop: true,
},
[Fade()]
);
Configuration Rationale:
align: 'center'
: Centers images in viewportcontainScroll: false
: Recommended for Fade pluginloop: true
: Enables infinite scrolling[Fade()]
: Applies fade transition plugin
5.2 Theme Integration
import { useColorMode } from '@docusaurus/theme-common';
const { colorMode } = useColorMode();
// Applied to root element
<div className={clsx(styles.gallery, colorMode === 'dark' && styles.dark)}>
5.3 Auto-play Implementation
useEffect(() => {
if (!emblaApi || !autoplay) return;
const interval = setInterval(() => {
emblaApi.scrollNext();
}, autoplay);
return () => clearInterval(interval);
}, [emblaApi, autoplay]);
Phase 6: CSS Architecture
6.1 Design System Integration
Leveraged Docusaurus CSS custom properties for consistent theming:
.embla__button {
box-shadow: inset 0 0 0 0.2rem var(--ifm-color-emphasis-300);
color: var(--ifm-color-content);
}
.embla__button:hover {
box-shadow: inset 0 0 0 0.2rem var(--ifm-color-primary);
color: var(--ifm-color-primary);
}
6.2 Responsive Strategy
/* Desktop: Side-by-side layout */
.embla__controls {
display: grid;
grid-template-columns: auto 1fr;
gap: 1.2rem;
}
/* Mobile: Stacked layout */
@media (max-width: 480px) {
.embla__controls {
grid-template-columns: 1fr;
gap: 0.8rem;
text-align: center;
}
}
6.3 SVG Icons Implementation
Extracted from reference embla.css
, optimized for accessibility:
<svg className={styles.embla__button__svg} viewBox="0 0 532 532">
<path
fill="currentColor"
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0..."
/>
</svg>
Phase 7: Accessibility Implementation
7.1 ARIA Labels
<button
aria-label="Previous image"
onClick={onPrevButtonClick}
disabled={prevBtnDisabled}
>
<button
aria-label={`Go to slide ${index + 1}`}
onClick={() => onDotButtonClick(index)}
>
7.2 Keyboard Navigation
Embla Carousel provides built-in keyboard support:
- Arrow keys for navigation
- Tab navigation for controls
- Enter/Space for button activation
Phase 8: Error Handling & Edge Cases
8.1 SSR Compatibility
const isBrowser = typeof window !== 'undefined';
if (!isBrowser) {
return (
<div className={clsx(styles.gallery, styles.fallback)}>
<div className={styles.fallbackGrid}>
{images.map((image, index) => (
<img key={index} src={image} alt={`Gallery image ${index + 1}`} />
))}
</div>
</div>
);
}
8.2 Empty State Handling
if (!images || images.length === 0) {
return (
<div className={styles.gallery}>
<div className={styles.emptyState}>No images to display</div>
</div>
);
}
Phase 9: Usage Implementation
9.1 Example Documentation Page
Created docs/gallery-example.md
with multiple use cases:
import FadeGallery from '@site/src/components/FadeGallery';
<FadeGallery
images={[
'/img/gallery/example-1.svg',
'/img/gallery/example-2.svg',
'/img/gallery/example-3.svg',
'/img/gallery/example-4.svg'
]}
title="Basic Image Gallery"
autoplay={3000}
/>
9.2 Asset Management
Created example SVG assets in /static/img/gallery/
:
- Scalable vector graphics for crisp display
- Gradient backgrounds with geometric shapes
- Consistent 800x400 dimensions
- Optimized file sizes
Phase 10: Performance Optimizations
10.1 Memory Management
useEffect(() => {
// ... setup logic
return () => {
// Comprehensive cleanup
emblaApi.off('reInit', onInit)
.off('reInit', onSelect)
.off('select', onSelect);
};
}, [emblaApi, onInit, onSelect]);
10.2 Render Optimization
useCallback
for all event handlers- Proper dependency arrays in
useEffect
- Conditional rendering for navigation controls
- CSS-based animations over JavaScript
Technical Decisions & Rationale
Why Embla Carousel?
- Library Agnostic: Framework-independent core
- Performance: Optimized for smooth animations
- Accessibility: Built-in keyboard and screen reader support
- Plugin System: Modular fade functionality
- TypeScript: First-class TypeScript support
Why Custom Hooks?
- Separation of Concerns: Logic isolated from presentation
- Reusability: Hooks can be used in other components
- Testing: Easier to unit test isolated logic
- Maintainability: Clear API boundaries
Why CSS Modules?
- Scope Isolation: Prevents style conflicts
- Theme Integration: Seamless Docusaurus integration
- Performance: No runtime CSS-in-JS overhead
- Maintainability: Co-located with component
Common Issues & Solutions
Issue: Fade Plugin Not Working
Solution: Ensure proper plugin configuration and options:
const [emblaRef] = useEmblaCarousel(
{ containScroll: false }, // Required for fade
[Fade()]
);
Issue: Navigation Buttons Not Responding
Solution: Verify emblaApi is properly initialized:
if (!emblaApi) return; // Guard clause in all handlers
Issue: SSR Hydration Mismatch
Solution: Implement proper browser detection:
const isBrowser = typeof window !== 'undefined';
Future Enhancements
- Lazy Loading: Implement intersection observer for images
- Gestures: Add swipe gesture support for mobile
- Lightbox: Modal view for full-size images
- Thumbnails: Thumbnail navigation option
- Animation Config: Customizable transition duration
Conclusion
This implementation demonstrates a production-ready image gallery component that leverages Embla Carousel's powerful features while maintaining consistency with Docusaurus design patterns. The component provides excellent user experience across all devices and accessibility requirements.
The architecture separates concerns effectively, making the codebase maintainable and extensible for future enhancements.