Jigsaw Puzzle
A photo, a playing video, or a live React component cut into die-cut cardboard jigsaw pieces. Scroll and the picture comes apart — every piece tumbles through 3D and settles face-up around the edge of the screen, casting its own shadow on the way. Scroll back and it reassembles. The interactive version keeps working while it is in pieces.
Included with membership
Members install Jigsaw Puzzle with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.
When to use it
A reveal that needs to be earned rather than watched — an about page that dismantles itself, a product shot that comes apart to show what is behind it, a section break with some theatre in it. It reads best given room and a slow scroll.
- Pick a grid from the two that read well: 5×4 is twenty pieces and stays legible as a picture coming apart, 8×6 is forty-eight and reads as a shatter. Every piece is its own canvas and its own transformed element, so going much past forty-eight costs more than it shows.
- Pass externalProgress when something else already owns the scroll. It takes 0 to 1 and disables the component's own wheel handling.
- Give it explicit width and height in HTML mode. There is no image to measure, so it falls back to 640×420.
- Use onProgressChange to drive anything that should move with the break-up — a caption, a colour, a counter. It reports the smoothed value, not the raw scroll.
- Serve media from your own domain. Pieces are drawn into canvases, and a cross-origin image that will not allow it leaves them blank.
Avoid:
- Do not put it somewhere the page also needs to scroll. With scrollDrive on it takes the wheel over itself, which is exactly what you want for a full-bleed section and exactly what you do not want mid-article.
- Do not use a long video. It is redrawn into every piece while it plays, so the cost is the piece count times the frame rate — short loops only.
- Do not expect the pieces to land inside the component. They settle around the perimeter of the viewport, so it wants to be the thing on screen.
- Do not reach for explosionForce to change where the pieces end up. It does not move the perimeter they settle on — it scales how high they fly, how hard they tumble, and how much they scatter around their landing spot. Around 3 is lively; the arrangement is the same either way.
Props
Whatever you put inside it gets cut into die-cut cardboard jigsaw pieces — a photograph, a playing video, or a live React component. Scroll and the picture comes apart: every piece lifts, tumbles through 3D and settles face-up around the edge of the frame, dropping a shadow as it goes. Scroll back and it reassembles, tab into socket. In HTML mode the thing being taken apart is still live — a form in pieces is still a form.
<JigsawPuzzle />
The whole effect — cutting, flight and reassembly. Works on an image, a video, or live children.
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | The image or video to cut up. Not needed in HTML mode, where the children are the subject. |
mode | "image" | "video" | "html" | "auto" | "auto" | What is being cut. "auto" reads the URL — anything ending .mp4 or .webm, or containing "video", is treated as video — and switches to HTML when children are passed without a src. |
children | React.ReactNode | — | The live DOM to cut up in HTML mode. It keeps working while it is in pieces, which is the point of that mode. |
rows | number | 6 for media, 4 for HTML | How many rows the grid is cut into. Reduced a step on weaker hardware unless you opt out. |
cols | number | 8 for media, 6 for HTML | How many columns. |
width | number | 720 for media, 640 for HTML | Width of the assembled picture in pixels. For media, height follows the aspect unless you set it. |
height | number | from the aspect, capped at 500; 420 for HTML | Height of the assembled picture in pixels. |
explosionForce | number | 1.8 | Scales how high a piece flies toward the viewer, how far it tumbles, and how much it scatters around its landing spot — but not the landing spot itself, which comes from the perimeter walk. Around 3 reads as lively. |
scrollDrive | boolean | true | Whether the wheel over the component drives the break-up. Ignored when externalProgress is supplied. |
externalProgress | number | — | Drives it from outside, 0 assembled to 1 fully dispersed. Supplying it takes the wheel handling out of play entirely. |
onProgressChange | (progress: number) => void | — | Fires as the smoothed progress moves. Useful for keeping other things in step. |
cardboardThickness | number | 3 | How thick each piece looks, in pixels. This is what makes them read as board rather than paper. |
bevelHighlight | boolean | true | Draws the specular rim along each die-cut edge. Off is flatter and slightly cheaper. |
disableAdaptiveQuality | boolean | false | Keeps the full grid regardless of what the device probe reports. Left off, weaker hardware gets fewer pieces. |
className | string | "" | Applied to the wrapper. |
style | React.CSSProperties | — | Inline styles on the wrapper. |
mount | "auto" | "eager" | "auto" | When to slice the image. The default waits until the page has frame headroom to spare. Set "eager" to cut the pieces immediately. Note this defaults the opposite way from the components that build a 3D scene: those start eagerly and take "auto" to defer. Both mean the same thing by "auto" — let the runtime pick the moment — the default just matches what each component already did. Neither setting takes it off the shared frame loop or overrides the reduced-motion preference. |