Skip to main content

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 wrapper
  • embla-carousel-fade@8.6.0: Fade transition plugin
  • embla-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

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

const [emblaRef, emblaApi] = useEmblaCarousel(
{
align: 'center',
containScroll: false,
loop: true,
},
[Fade()]
);

Configuration Rationale:

  • align: 'center': Centers images in viewport
  • containScroll: false: Recommended for Fade plugin
  • loop: 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

  1. Library Agnostic: Framework-independent core
  2. Performance: Optimized for smooth animations
  3. Accessibility: Built-in keyboard and screen reader support
  4. Plugin System: Modular fade functionality
  5. TypeScript: First-class TypeScript support

Why Custom Hooks?

  1. Separation of Concerns: Logic isolated from presentation
  2. Reusability: Hooks can be used in other components
  3. Testing: Easier to unit test isolated logic
  4. Maintainability: Clear API boundaries

Why CSS Modules?

  1. Scope Isolation: Prevents style conflicts
  2. Theme Integration: Seamless Docusaurus integration
  3. Performance: No runtime CSS-in-JS overhead
  4. 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

  1. Lazy Loading: Implement intersection observer for images
  2. Gestures: Add swipe gesture support for mobile
  3. Lightbox: Modal view for full-size images
  4. Thumbnails: Thumbnail navigation option
  5. 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.