Fold Carousel
A carousel that folds from one slide to the next, like a two-panel phone. The image stays flat and the frame changes shape around it.




Installation
Needs a project set up with shadcn. Adds components/ui/fold-carousel and installs motion, lucide-react, and cn.
npx shadcn@latest add Kareem-AEz/iphone-duo-fold-carousel/fold-carouselUsage
import type { ReactNode } from "react";
import {
FoldCarousel,
FoldCarouselFrame,
FoldCarouselNext,
FoldCarouselPrevious,
} from "@/components/ui/fold-carousel/fold-carousel";
export function DoorExample({ slides }: { slides: ReactNode[] }) {
return (
<FoldCarousel
slides={slides}
aria-label="Landscapes"
className="flex flex-col items-center gap-20"
>
<FoldCarouselFrame panelWidth="min(20rem, 42vw)" />
<div className="flex gap-2">
<FoldCarouselPrevious />
<FoldCarouselNext />
</div>
</FoldCarousel>
);
}Examples
Book
The next slide lies underneath and the fold uncovers it, like turning a page.




import type { ReactNode } from "react";
import {
FoldCarousel,
FoldCarouselFrame,
FoldCarouselNext,
FoldCarouselPrevious,
} from "@/components/ui/fold-carousel/fold-carousel";
export function BookExample({ slides }: { slides: ReactNode[] }) {
return (
<FoldCarousel
slides={slides}
aria-label="Landscapes"
className="flex flex-col items-center gap-14"
>
<FoldCarouselFrame variant="book" panelWidth="min(14rem, 38vw)" />
<div className="flex gap-2">
<FoldCarouselPrevious />
<FoldCarouselNext />
</div>
</FoldCarousel>
);
}Custom controls, no loop
useFoldCarousel drives your own dots. Without a loop, the buttons disable at either end.




"use client";
import type { ReactNode } from "react";
import {
FoldCarousel,
FoldCarouselFrame,
FoldCarouselNext,
FoldCarouselPrevious,
} from "@/components/ui/fold-carousel/fold-carousel";
import { useFoldCarousel } from "@/components/ui/fold-carousel/use-fold-carousel";
export function ControlsExample({ slides }: { slides: ReactNode[] }) {
return (
<FoldCarousel
slides={slides}
loop={false}
aria-label="Landscapes"
className="flex flex-col items-center gap-14"
>
<FoldCarouselFrame panelWidth="min(14rem, 38vw)" />
<div className="flex items-center gap-2">
<FoldCarouselPrevious size="icon-sm" />
<Dots />
<FoldCarouselNext size="icon-sm" />
</div>
</FoldCarousel>
);
}
function Dots() {
const { index, count, goTo } = useFoldCarousel();
return (
<div className="flex">
{Array.from({ length: count }, (_, slide) => (
<button
key={slide}
type="button"
aria-label={`Slide ${slide + 1} of ${count}`}
aria-current={slide === index}
onClick={() => goTo(slide)}
className="group focus-visible:ring-ring/50 grid size-8 cursor-pointer place-items-center rounded-full outline-none focus-visible:ring-3"
>
<span className="bg-foreground/20 group-hover:bg-foreground/40 group-aria-current:bg-foreground size-1.5 rounded-full transition-colors" />
</button>
))}
</div>
);
}Try your photos




Pick 2 to 8. Your photos never leave this tab, and the frame crops them to 2:1.
How it works
The slide never rotates. Each face stays flat and is clipped to the outline a turning leaf would have, then blurred and shaded by how far it has tilted. Drag to scrub one fold.
API
FoldCarousel
Holds the state. Put a frame and any controls inside it. Takes every div prop.
| Prop | Type | Default |
|---|---|---|
slidesOne slide per item. Anything that fills a box works, like img or next/image with fill. | ReactNode[] | – |
loopWhether the last slide folds on to the first. | boolean | true |
durationSeconds a turn takes to settle onto a slide. | number | 0.8 |
defaultIndexThe slide shown first. | number | 0 |
onIndexChangeCalled with the slide a turn is heading for, as soon as it starts. | (index: number) => void | – |
FoldCarouselFrame
The folding picture. Takes every div prop.
| Prop | Type | Default |
|---|---|---|
variantdoor swings the next slide in behind the current one. book lays it underneath, like a page. | "door" | "book" | "door" |
panelWidthWidth of one square panel, as any CSS length. The frame is two panels wide. | string | "min(23rem, 42vw)" |
depthPerspective distance, in panel widths. Lower is more dramatic. Must stay above 1. | number | 3.83 |
bezelColorAny CSS color. Shows on a leaf's edge mid-turn, and in place of a slide until its images have loaded. | string | "black" |
FoldCarouselPrevious, FoldCarouselNext
shadcn Buttons. They take every Button prop, children replace the icon, and they stay focusable when disabled.
useFoldCarousel
Reads the surrounding carousel, for building your own controls.
| Value | Type |
|---|---|
indexThe slide a turn is heading for. | number |
countHow many slides there are. | number |
canPrev, canNextFalse at the ends when loop is off. | boolean |
prev, nextFolds one slide back or on. | () => void |
goToFolds to a slide, the short way round when looping. | (slide: number) => void |
Accessibility
- Arrow keys change slides while focus is inside the carousel, including after clicking a slide.
- Screen readers hear each slide once, and a live region announces the slide on change.
- The hidden copies of each slide are inert, so Tab never lands on them.
- Previous and next stay focusable at the ends when loop is off.
- With reduced motion turned on, slides cut instead of folding.