Jigsaw Puzzle

ProScroll3DWebGLpowered by @vectorvesper/motion

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.

Jigsaw Puzzle

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.

PropTypeDefaultDescription
srcstring—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.
childrenReact.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.
rowsnumber6 for media, 4 for HTMLHow many rows the grid is cut into. Reduced a step on weaker hardware unless you opt out.
colsnumber8 for media, 6 for HTMLHow many columns.
widthnumber720 for media, 640 for HTMLWidth of the assembled picture in pixels. For media, height follows the aspect unless you set it.
heightnumberfrom the aspect, capped at 500; 420 for HTMLHeight of the assembled picture in pixels.
explosionForcenumber1.8Scales 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.
scrollDrivebooleantrueWhether the wheel over the component drives the break-up. Ignored when externalProgress is supplied.
externalProgressnumber—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.
cardboardThicknessnumber3How thick each piece looks, in pixels. This is what makes them read as board rather than paper.
bevelHighlightbooleantrueDraws the specular rim along each die-cut edge. Off is flatter and slightly cheaper.
disableAdaptiveQualitybooleanfalseKeeps the full grid regardless of what the device probe reports. Left off, weaker hardware gets fewer pieces.
classNamestring""Applied to the wrapper.
styleReact.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.
It reads the visitor's reduced-motion preference and flattens the flight — pieces travel to exactly the same places without lifting toward the viewer or tumbling on the way, and the progress tracks the scroll directly instead of easing behind it. The picture still comes apart and goes back together; what goes is the somersault.
Each piece is its own canvas and its own 3D-transformed element, so cost follows rows × cols. Static images are only redrawn while the progress is actually moving; video repaints every frame it plays, and pauses itself when it is not needed. Grid size steps down on weaker hardware unless disableAdaptiveQuality is set. Transforms are written straight to the elements, so nothing here re-renders React as it moves.
Client component. It measures media and draws to canvases on mount, so it has to run in the browser.
It draws on the shared frame loop rather than starting one of its own, so putting several animated components on a page costs one callback, not one each, and the scheduler can set decorative work aside when frames get tight. Slicing the image is the expensive moment, so it waits for the page to have frame headroom before doing it. A static image only repaints while pieces are actually moving, which means a settled puzzle costs nothing per frame.
Back to all components