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.onSnap(index: number) => void (default: undefined)
A callback that is called when the carousel snaps to an item.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-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)
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)to programmatically move a specific item into view. - The carousel may run into an infinite loop in certain conditions. This has only been observed on iPhones with 120Hz displays (13+) while quickly swiping left and right. The issue is duct-tape-fixed so that the carousel doesn’t get stuck in an infinite loop anymore, but the actual bug has not been identified yet.
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;
}