Carousel Engine is a flexible, adapter-driven Vue 3 carousel framework that leverages the Adapter Pattern to separate data transformation from rendering. This is a forked and re-architected version of @rei/recommendations-slider
, designed for greater modularity and reusability.
$ npm i @rei/carousel-engine
Using CarouselEngine involves three key steps:
- Define an Adapter (Transforms raw model data into a carousel format)
- Provide a Model (Pass structured data to the carousel)
- Handle Events (Optional) (Define custom event handlers for interactions)
An adapter maps raw model data to a structured carousel configuration.
// adapter.ts
import type { LifestyleModel, LifestyleSlide } from '.';
import type {
CarouselAdapter,
CarouselConfig,
Slide,
} from '@rei/carousel-engine';
import SlideComponent from './LifestyleSlide.vue';
export const adapter: CarouselAdapter<LifestyleSlide> = (modelData) => {
/**
* Extracts slides from the raw model data.
*/
const {
slides: slideItems = [],
slideStyle,
slidesVisible = 4,
} = modelData as Partial<LifestyleModel>;
/**
* Determines the carousel ID.
*/
const carouselId = 'lifestyle';
/**
* Transforms raw items into an array of slides for the carousel.
*
* @type {Slide<LifestyleSlide>[]}
*/
const slides: Slide<LifestyleSlide>[] = Array.isArray(slideItems)
? slideItems.map((slide, index) => ({
key: `lifestyle-slide-${index}`,
props: {
...slide,
slideStyle,
lastSlide: index === slideItems.length - 1,
},
}))
: [];
/**
* Constructs the carousel config with the resolved slides and metadata.
*
* @type {CarouselConfig<LifestyleSlide>}
*/
const carouselConfig: CarouselConfig<LifestyleSlide> = {
component: SlideComponent,
slides,
carouselId,
description: 'Lifestyle carousel',
slidesGap: parseInt(CdrSpaceThreeQuarterX, 10),
slidesToShow: slidesVisible,
focusSelector: ':first-child a',
};
return carouselConfig;
};
export default adapter;
In a Vue component, pass the model and adapter to CarouselEngine. Your model is passed to your adapter, which transforms it into a structured carousel configuration.
<template>
<CarouselEngine :model="model" :adapter="adapter" />
</template>
<script setup lang="ts">
import CarouselEngine from '@rei/carousel-engine';
import adapter from './adapter';
import lifestyleModelData from './mock.json';
import type { LifestyleModel } from './implementation/Lifestyle';
const model = lifestyleModelData as LifestyleModel;
</script>
Carousel Engine emits several named events:
arrowClick
- Emitted when an arrow is clicked.resize
- Emitted when the carousel resize observer fires.
// handlers.ts
import type { CarouselArrowClickPayload } from '@rei/carousel-engine';
import type { LifestyleModel, LifestyleSlideClickPayload } from '.';
/**
* Handles arrow click events in the carousel.
* Determines scroll direction and formats analytics tracking data.
*
* @param {unknown} payload - The event payload containing navigation details.
* @return {void}
*/
export function onArrowClick(payload: unknown): void {
const { direction, event, model = {} } = payload as CarouselArrowClickPayload;
const { slidesVisible, slideStyle } = model as Partial<LifestyleModel>;
const scrollDirection =
direction === 'right' ? 'forwardScroll' : 'backScroll';
const scrollValue = `scroll-${direction}`;
const analytics = {
[scrollDirection]: scrollValue, // Scroll direction tracking key
slidesVisible,
slideStyle,
};
console.log('onArrowClick', { event, direction, analytics });
}
Attach the handler:
<template>
<CarouselEngine
:model="model"
:adapter="adapter"
@arrow-click="onArrowClick"
/>
</template>
<script setup lang="ts">
import CarouselEngine from '@rei/carousel-engine';
import { onSlideClick, onArrowClick } from './handlers';
</script>
There is a default resizing strategy that you can enable for general use. Enable it by setting useDefaultResizeStrategy
to true
from your implementation's adapter. It is disabled by default.
// adapter.ts
...
/**
* Constructs the carousel config with the resolved slides and metadata.
*
* @type {CarouselConfig<LifestyleSlide>}
*/
const carouselConfig: CarouselConfig<LifestyleSlide> = {
component: SlideComponent,
slides,
carouselId,
description: 'Lifestyle carousel',
slidesGap: parseInt(CdrSpaceThreeQuarterX, 10),
slidesToShow: slidesVisible,
focusSelector: ':first-child a',
useDefaultResizeStrategy: true, // <-- here
};
return carouselConfig;
...
If you need to customize the resize strategy, you can attach a handler to the resize
event. The resize event is emitted when the carousel resize observer fires. It provides the slidesToShow
and slidesToScroll
references which can be used to update the carousel's internal state.
Define a handler:
/**
* Handles window resize events and updates the carousel's
* internal state for `slidesToShow` and `slidesToScroll` based
* on the current window size.
*
* @param {unknown} payload - The event payload containing
* information about the carousel's config and model.
* @return {void}
*/
export function onResize(payload: unknown): void {
const {
slidesToScroll,
slidesToShow,
model = {},
} = payload as CarouselResizePayload;
const { slidesVisible = 3 } = model as Partial<LifestyleModel>;
const { clientWidth } = window.document.body;
switch (true) {
case clientWidth >= Number(CdrBreakpointLg):
slidesToShow.value = slidesVisible;
slidesToScroll.value = slidesVisible - 1;
break;
case clientWidth >= Number(CdrBreakpointMd):
slidesToShow.value = 3;
slidesToScroll.value = 2;
break;
default:
slidesToShow.value = 2;
slidesToScroll.value = 1;
}
}
Attach the handler:
<template>
<CarouselEngine
:model="model"
:adapter="adapter"
@arrow-click="onArrowClick"
@resize="onResize"
/>
</template>
<script setup lang="ts">
import CarouselEngine from '@rei/carousel-engine';
import { onSlideClick, onArrowClick, onResize } from './handlers';
</script>
Carousel Engine supports custom events by using Provide/Inject. If you need to bubble an event to a parent component, CarouselEngine.vue
provides the emitEvent
function. In your components, you can Inject the emitEvent
function and use it to emit custom events.
<template>
<button class="slide__button" @click.once="onSlideClick"></button>
</template>
<script setup lang="ts">
import type { CarouselEventEmitter } from '@rei/carousel-engine';
import { CarouselEventKey } from '@rei/carousel-engine';
const emitEvent = inject(CarouselEventKey) as CarouselEventEmitter;
/**
* Handles the click event on a slide, emitting a 'slideClick' event with the event details and the slide item.
*
* @param {Event} event - The click event that triggered this function.
* @return {void}
*/
const onSlideClick = (event: Event) => {
emitEvent?.('slideClick', {
event,
item: props,
} as LifestyleSlideClickPayload);
};
</script>
Now, define a handler for the slideClick
event:
import type { CarouselArrowClickPayload } from '@rei/carousel-engine';
import type { LifestyleModel, LifestyleSlideClickPayload } from '.';
/**
* Handles slide click events in the carousel and logs analytics data.
*
* @param {unknown} payload - The event payload containing details about the clicked slide.
* @return {void}
*/
export function onSlideClick(payload: unknown): void {
const { event, item } = payload as LifestyleSlideClickPayload;
const analytics = {
target: item.cta.target,
text: item.cta.text,
};
console.log('onSlideClick', { event, item, analytics });
}
Attach the handler:
<template>
<CarouselEngine
:model="lifestyleModelData"
:adapter="LifestyleAdapter"
@slide-click="onSlideClick"
@arrow-click="onArrowClick"
@resize="onResize"
/>
</template>
<script setup lang="ts">
import CarouselEngine from 'carousel-engine';
import LifestyleAdapter from './adapter';
import lifestyleModelData from './mock.json';
import { onSlideClick, onArrowClick, onResize } from './handlers';
</script>
Carousel Engine is built with a layered architecture that separates concerns into four key layers:
- The Carousel Engine component acts as the orchestrator, managing carousel state, event handling, and rendering.
- Handles carousel navigation, focus management, and slide transitions, while remaining agnostic to specific implementations.
- Adapters define how raw model data is transformed into a standardized carousel structure.
- Implements the Adapter Pattern, allowing different data formats to be consumed without modifying the core engine.
- Handlers allow consumers to define custom event logic for slides and navigation without modifying the core engine.
- Arbitrary events can be emitted with Provide/Inject, enabling per-instance overrides.
- Supports tracking analytics, logging, or triggering additional UI behaviors in a fully decoupled way.
- The entire carousel is driven by a structured model, making it highly flexible.
- Consumers pass a model object and an adapter, and CarouselEngine dynamically constructs the carousel.
- Enables easy A/B testing, content variations, and dynamic configurations without modifying components.
Check out the examples
folder for more detailed examples. You can also run the local development environment to see how it works in SFCs:
git clone [email protected]:rei/carousel-engine.git
cd carousel-engine
npm ci
npm run dev