Carousel
A simple, responsive, infinite carousel component with zero dependencies. It supports touch gestures, mouse dragging and mouse wheels. It can also be turned into an autoscrolling ticker:
🎶Youspinmerightroundbabyrightroundlikearecordbabyrightroundroundround🎶Youspinmerightroundbabyrightroundlikearecordbabyrightroundroundround
Installation
npm install @madeinhaus/carouselImport
import Carousel from '@madeinhaus/carousel';
import '@madeinhaus/carousel/dist/index.css';Props
All props are optional.
activeItemIndexnumber (default: 0)
The index of the active item on mount.align“start” | “center” (default: “start”)
The alignment of the active item.asReact.ElementType (default: ‘ul’)
The element type to use for the container element.childAsReact.ElementType (default: ‘li’)
The element type to use for the item wrapper elements.classNamestring (default: undefined)
The CSS class name to apply to the container element.dampingnumber (default: 200)
The damping factor applied to the velocity of the carousel after throwing. Lower values make the carousel stop more abruptly.direction“horizontal” | “vertical” (default: “horizontal”)
The scroll direction.
If set to"vertical",enableVerticalScrollmust be set totrue.disableSnapboolean (default: false)
Whether to disable snapping. If snapping is disabled, the carousel will not snap to the--carousel-snap-position.enableNavigationGesturesboolean (default: false)
Mobile browsers often implement gestures to navigate back or forward in the browser history (usually by swiping from the edges of the screen). These gestures can interfere with the carousel’s touch gestures and are therefore disabled by default. If this prop is set totrue, the carousel will re-enable these gestures.enableVerticalScrollboolean (default: false)
Whether to enable vertical scrolling. If enabled, the carousel will scroll horizontally when swiping or scrolling up or down (or scroll vertically whendirectionis set to"vertical").itemClassNamestring (default: undefined)
The CSS class name to apply to the item wrapper elements.onDrag() => void (default: undefined)
A callback that is called when the user starts dragging the carousel.onPress(event: PointerEvent) => void (default: undefined)
A callback that is called when the user presses on the carousel.onPosition(data: PositionData) => void (default: undefined)
A callback that is called on every position update with visibility metadata. It receives a PositionData object:containerSize: number — The size of the carousel container element.snapPosition: number — Specifies where within the container items should snap into place.gap: number — The gap between items.visibleItems: VisibleItem[] — Array of currently visible items. Each item is an object containingindex(number),element(HTMLElement),startPos(number), andendPos(number).
onSnap(index: number, element: HTMLElement) => void (default: undefined)
A callback that is called when the carousel snaps to an item. It receives the snapped item’sindexand itsHTMLElement.styleReact.CSSProperties (default: undefined)
The CSS style to apply to the container element.
CSS Variables
Responsive props are passed to the carousel via CSS variables. Define these on the carousel’s container element or on one of its ancestors.
--carousel-gaplength value (default: 0)
The gap between carousel items.--carousel-item-sizelength value (default: 0)
The width (or height for vertical carousels) of the carousel items. Set this to a length value !== 0 if all items are the same width (or height). This helps with performance because the carousel doesn’t have to measure the size of each item.--carousel-snap-positionlength value (default: 0)
If snapping is enabled, this defines the position (measured from the left edge of the carousel’s container) that an item snaps to. Use this in combination with thealignprop to snap the left edge (align="start") or the center (align="center") of an item to this position.--carousel-autoscrollnumber (default: 0)
Set this to a value !== 0 to turn the carousel into an autoscrolling ticker. The value is the speed in px/ms. Values between +/- 0.1 and 0.2 work best. Negative values make the carousel scroll to the left, positive values to the right.--carousel-autoadvancenumber (default: 0)
Set to1or-1to automatically advance the carousel step by step in the specified direction.--carousel-autoadvance-delaytime value (default: 5s)
The delay between automatic advances. Supports time units (e.g.5sor5000ms).--carousel-autoadvance-durationtime value (default: 700ms)
The duration of the step animation. Supports time units (e.g.700msor0.7s).--carousel-disabled0 | 1 (default: 0)
Set this to 1 to disable the carousel (disables all carousel JS and all internal carousel CSS). Useful e.g. if on large screens you want to layout items on a (static) grid and on small screens in a carousel.
Length values can be any valid CSS length value, e.g. 1rem, 100px, 50%,
calc(var(--grid-margin) * 2). Relative length values are resolved relative to the size
of the carousel’s container.
Imperative Methods
You may use the following methods via ref to programmatically control the carousel:
moveIntoView(index, options)
Moves the item at the given index into view.
Theoptionsobject takes the following keys:
easeFnEasingFunction (optional, default: easings.easeInOutCubic)durationnumber (optional, default: 700)
next(duration)
Programmatically moves the carousel to the next item. Thedurationin milliseconds is optional (default: 700).prev(duration)
Programmatically moves the carousel to the previous item. Thedurationin milliseconds is optional (default: 700).refresh()
Re-renders the carousel. Useful if any of the CSS variables have changed programmatically (not triggered by resize).
Tips
- To center the active item in the container, set the
alignprop tocenterand--carousel-snap-positionto50%. The default is to align the left edge of the active item with the left edge of the container. - The children you pass to the carousel are internally wrapped, by default in a
<li>element. If you want to use a different element, pass it as thechildAsprop. To style the wrapper element, use theitemClassNameprop. Example:<Carousel childAs="span" itemClassName={styles.itemWrapper}>. - See Carousel.module.scss for the default CSS applied to the container and the item wrappers.
Known Issues
- The carousel is not accessible. It doesn’t support keyboard navigation and it doesn’t announce its state to screen readers.
- The carousel can currently not be used as a controlled component. Even though it has a
activeItemIndexprop, it is not used to control the active item (it can only be used to initialize the active item on mount). Instead, usemoveIntoView(index, options)ornext(duration)orprev(duration)to programmatically move the carousel items.
Examples
Basic
CarouselDemo.tsx
import Carousel from '@madeinhaus/carousel';
import '@madeinhaus/carousel/dist/index.css';
import styles from './CarouselDemo.module.css';
const dog = '/assets/images/dogs/n02097047_1028.jpg';
const dogs = new Array(20).fill(dog);
const CarouselDemo: React.FC = () => {
return (
<div className={styles.root}>
<Carousel className={styles.carousel} itemClassName={styles.carouselItem}>
{dogs.map((dog, i) => (
<img key={i} src={dog} alt="" className={styles.image} />
))}
</Carousel>
</div>
);
};
export default CarouselDemo;CarouselDemo.module.css
.carousel {
--image-size: 300px;
--carousel-gap: 20px;
--carousel-item-size: var(--image-size);
}
.carouselItem {
position: relative;
width: var(--image-size);
height: var(--image-size);
}
.image {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}Lazy Loading
CarouselDemoLazyLoad.tsx
import Carousel from '@madeinhaus/carousel';
import '@madeinhaus/carousel/dist/index.css';
import cx from 'clsx';
import { useIntersectionObserver, useImagePreload } from '@madeinhaus/hooks';
import styles from './Carousel.module.css';
const dog = '/assets/images/dogs/n02097047_1028.jpg';
const dogs = new Array(20).fill(dog);
const CarouselDemo: React.FC = () => {
return (
<div className={styles.root}>
<Carousel className={styles.carousel} itemClassName={styles.carouselItem}>
{dogs.map((dog, i) => (
<LazyImage key={i} url={dog} />
))}
</Carousel>
</div>
);
};
interface LazyImageProps {
url: string;
}
const LazyImage: React.FC<LazyImageProps> = ({ url }) => {
const [inView, intersectionRef] = useIntersectionObserver();
const [loaded, loadRef] = useImagePreload();
return (
<div ref={intersectionRef} className={styles.imageWrapper}>
<img
ref={loadRef}
src={inView ? url : undefined}
className={cx(styles.image, { [styles.loaded]: loaded })}
alt=""
/>
</div>
);
};
export default CarouselDemo;CarouselDemoLazyLoad.module.css
.carousel {
--image-size: 300px;
--carousel-gap: 20px;
--carousel-item-size: var(--image-size);
}
.carouselItem {
position: relative;
width: var(--image-size);
height: var(--image-size);
}
.imageWrapper {
position: absolute;
inset: 0;
}
.image {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.001;
transition: opacity 0.5s linear;
}
.image.loaded {
opacity: 1;
}