@mshafiqyajid/react-carousel
Headless carousel hook and styled slide component for React. Touch swipe, drag, keyboard navigation, autoplay with pause-on-hover/focus, fade or slide transition, peek adjacent slides, counter overlay, autoplay progress bar, multi-slide view, dots, arrows, controlled/uncontrolled index, full ARIA compliance, and smooth motion with reduced-motion support.
Playground #
import { CarouselStyled } from "@mshafiqyajid/react-carousel/styled";
import "@mshafiqyajid/react-carousel/styles.css";
<CarouselStyled
items={slides}
/>Install #
npm install @mshafiqyajid/react-carousel Quick start #
import { CarouselStyled } from "@mshafiqyajid/react-carousel/styled";
import "@mshafiqyajid/react-carousel/styles.css";
const SLIDES = [
{
src: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&q=80",
alt: "Swiss Alps at golden hour",
caption: "Swiss Alps β golden hour above the clouds",
},
{
src: "https://images.unsplash.com/photo-1493246507139-91e8fad9978e?w=800&q=80",
alt: "Moraine Lake, Banff",
caption: "Moraine Lake, Banff β Canadian Rockies",
},
{
src: "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&q=80",
alt: "Morning forest light",
caption: "Morning light, Black Forest, Germany",
},
];
const slides = SLIDES.map((s) => (
<div key={s.src} style={{ position: "relative", width: "100%", height: "100%" }}>
<img
src={s.src}
alt={s.alt}
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
/>
<div style={{
position: "absolute", bottom: 0, left: 0, right: 0,
padding: "2rem 1.25rem 1rem",
background: "linear-gradient(to top, rgba(0,0,0,0.55), transparent)",
}}>
<p style={{ margin: 0, color: "#fff", fontSize: "0.8125rem", fontWeight: 500 }}>
{s.caption}
</p>
</div>
</div>
));
export function Gallery() {
return (
<div style={{ width: "100%", height: 360, borderRadius: 12, overflow: "hidden" }}>
<CarouselStyled
items={slides}
loop
showDots
showArrows
autoPlay
autoPlayInterval={4000}
counter
style={{ width: "100%", height: "100%" }}
/>
</div>
);
} Controlled index #
Use index and onIndexChange for full control over the active slide.
import { useState } from "react";
import { CarouselStyled } from "@mshafiqyajid/react-carousel/styled";
import "@mshafiqyajid/react-carousel/styles.css";
export function ControlledCarousel() {
const [index, setIndex] = useState(0);
return (
<>
<p>Active: {index}</p>
<CarouselStyled
items={slides}
index={index}
onIndexChange={setIndex}
/>
</>
);
} Multi-slide view #
Show multiple slides at once with slidesPerView and spacing.
<CarouselStyled
items={slides}
slidesPerView={2}
spacing={16}
loop
/> Headless usage #
import { useCarousel } from "@mshafiqyajid/react-carousel";
function MyCarousel({ items }) {
const {
containerProps,
trackProps,
getSlideProps,
prevProps,
nextProps,
getDotProps,
index,
total,
} = useCarousel({
items,
loop: true,
autoPlay: true,
autoPlayInterval: 3000,
});
return (
<div {...containerProps} style={{ overflow: "hidden", position: "relative" }}>
<div {...trackProps} style={{ display: "flex", ...trackProps.style }}>
{items.map((item, i) => (
<div key={i} {...getSlideProps(i)}>{item}</div>
))}
</div>
<button {...prevProps}>β</button>
<button {...nextProps}>β</button>
<div>
{Array.from({ length: total }, (_, i) => (
<button key={i} {...getDotProps(i)}>
{i === index ? "β" : "β"}
</button>
))}
</div>
</div>
);
} Keyboard #
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight | Previous / next slide (horizontal) |
| ArrowUp / ArrowDown | Previous / next slide (vertical) |
| Home | First slide |
| End | Last slide |
| Tab | Focus prev/next buttons |
Props #
| Prop | Type | Default | Description |
|---|---|---|---|
| items | ReactNode[] | β | Slides to render |
| index | number | β | Controlled active index |
| defaultIndex | number | 0 | Uncontrolled initial index |
| onIndexChange | (index: number) => void | β | Called when index changes |
| orientation | "horizontal" | "vertical" | "horizontal" | Slide axis |
| loop | boolean | true | Wrap around at boundaries |
| autoPlay | boolean | false | Auto-advance slides |
| autoPlayInterval | number | 4000 | Auto-advance interval in ms |
| pauseOnHover | boolean | true | Pause autoPlay on hover |
| pauseOnFocus | boolean | true | Pause autoPlay on focus |
| slidesPerView | number | 1 | Slides visible at once |
| spacing | number | string | 0 | Gap between slides |
| showDots | boolean | true | Show dot indicators |
| showArrows | boolean | true | Show prev/next arrow buttons |
| swipe | boolean | true | Enable touch swipe |
| drag | boolean | true | Enable mouse drag |
| dragThreshold | number | 50 | Min px distance to commit swipe |
| onSwipeStart | () => void | β | Called when drag starts |
| onSwipeEnd | (committed: boolean) => void | β | Called when drag ends |
| renderDot | (index, active) => ReactNode | β | Custom dot renderer |
| renderArrow | (dir, disabled) => ReactNode | β | Custom arrow renderer |
| className | string | β | Extra class on root element |
| style | CSSProperties | β | Inline style on root element |
| ref | Ref<HTMLDivElement> | β | Forwarded to root wrapper div |
| transitionEffect | "slide" | "fade" | "slide" | Slide transition style |
| peek | number | string | 0 | Show a sliver of adjacent slides (px or CSS string like "10%") |
| counter | boolean | false | Show "N / M" counter overlay (top-right) |
| autoPlayProgress | boolean | false | Show a progress bar filling over each autoPlay interval |
Fade transition #
Set transitionEffect="fade" to crossfade between slides instead of sliding. Drag/swipe still works β dragging past the threshold commits the slide change.
<CarouselStyled
items={slides}
transitionEffect="fade"
showDots
autoPlay
/> Peek #
Pass a peek value (number in px or a CSS string) to reveal the edges of the previous and next slides. The root element still clips, but the viewport lets adjacent slides bleed through.
<CarouselStyled
items={slides}
peek={40}
loop
/> Counter overlay #
Enable counter to show a frosted-glass "N / M" pill in the top-right corner β useful when dots are hidden or as an additional visual cue.
<CarouselStyled
items={slides}
counter
showDots={false}
/> AutoPlay progress bar #
When autoPlay is enabled, add autoPlayProgress to show a thin bar at the bottom of the carousel that fills over the interval. It resets on each slide change and pauses when the carousel is paused.
<CarouselStyled
items={slides}
autoPlay
autoPlayInterval={4000}
autoPlayProgress
/> CSS variables #
:root {
--rcar-radius: 12px;
--rcar-arrow-size: 2.5rem;
--rcar-dot-size: 8px;
--rcar-dot-size-active: 24px;
--rcar-dot-gap: 6px;
--rcar-arrow-bg: rgba(255, 255, 255, 0.92);
--rcar-arrow-fg: #18181b;
--rcar-dot-bg: rgba(0, 0, 0, 0.22);
--rcar-dot-bg-active: #18181b;
--rcar-duration: 300ms;
--rcar-ease: cubic-bezier(0.32, 0.72, 0, 1);
}