Infinite Drag Canvas
An infinitely pannable portfolio canvas that scatters cards across a boundless space, with drag (including momentum), keyboard panning, and one-click recentering.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/infinite-drag-canvas.en.json?t=<install token>"Infinite Drag Canvas is a React infinite canvas component built with Tailwind CSS and Motion. You place cards at world coordinates with the canvas center as the origin; users drag to pan, the canvas keeps gliding with momentum after release, and the dot grid moves along so the space reads as boundless.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<InfiniteDragCanvas/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- A portfolio homepage for a designer or studio, with work scattered across a large draggable space.
- Mood boards, inspiration walls or event photo walls where there is a lot of content and a fixed grid feels too rigid.
- An interactive showcase on a product launch or brand campaign page that visitors explore by dragging.
- Displaying whiteboard-style concept maps or flow sketches (view only; the component has no zoom or editing).
Installation
npx webberui@latest add "https://webberui.com/r/infinite-drag-canvas.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/infinite-drag-canvas.
Usage
import {
InfiniteDragCanvas,
CanvasItem,
} from "@/components/ui/infinite-drag-canvas";
<InfiniteDragCanvas className="h-[400px]">
<CanvasItem x={-120} y={-40}>
<div className="rounded-xl border bg-white p-4 shadow">Aurora</div>
</CanvasItem>
<CanvasItem x={140} y={80}>
<div className="rounded-xl border bg-white p-4 shadow">Drift</div>
</CanvasItem>
</InfiniteDragCanvas>;The center of the canvas is the world-space origin (0, 0); a CanvasItem aligns its own center to the x / y you give it and pans along with the canvas.
Examples
Placing work cards from data
Store each piece's coordinates in your data and map them to CanvasItems. Cards disable pointer events while a drag is in progress, so links inside them are not triggered when the mouse is released.
import {
InfiniteDragCanvas,
CanvasItem,
} from "@/components/ui/infinite-drag-canvas";
const works = [
{ id: "brand", title: "Brand identity refresh", href: "/work/brand", x: -260, y: -120 },
{ id: "app", title: "Budgeting app UI", href: "/work/app", x: 40, y: -180 },
{ id: "site", title: "E-commerce site", href: "/work/site", x: 220, y: 60 },
{ id: "poster", title: "Exhibition key visual", href: "/work/poster", x: -140, y: 160 },
];
export function PortfolioCanvas() {
return (
<InfiniteDragCanvas className="h-[480px]">
{works.map((w) => (
<CanvasItem key={w.id} x={w.x} y={w.y}>
<a
href={w.href}
className="block w-48 rounded-xl border bg-white p-4 text-sm font-medium shadow-sm"
>
{w.title}
</a>
</CanvasItem>
))}
</InfiniteDragCanvas>
);
}Controlled mode: jump to a piece
Pass offset and onOffsetChange to manage the pan position from outside. To bring the item at (x, y) to the center of the canvas, set offset to { x: -x, y: -y }; a controlled update jumps straight to that position.
"use client";
import * as React from "react";
import {
InfiniteDragCanvas,
CanvasItem,
type CanvasOffset,
} from "@/components/ui/infinite-drag-canvas";
export function JumpToWork({ works }: { works: { id: string; title: string; x: number; y: number }[] }) {
const [offset, setOffset] = React.useState<CanvasOffset>({ x: 0, y: 0 });
return (
<div className="space-y-3">
<div className="flex gap-2">
{works.map((w) => (
<button key={w.id} onClick={() => setOffset({ x: -w.x, y: -w.y })}>
{w.title}
</button>
))}
</div>
<InfiniteDragCanvas offset={offset} onOffsetChange={setOffset}>
{works.map((w) => (
<CanvasItem key={w.id} x={w.x} y={w.y}>
<div className="rounded-xl border bg-white p-4 shadow-sm">{w.title}</div>
</CanvasItem>
))}
</InfiniteDragCanvas>
</div>
);
}Tuning the background and feel
gridSize sets the dot spacing and showGrid={false} removes the dots; momentum={false} makes the canvas stop as soon as it is released; keyboardStep sets how far each arrow-key press pans; showRecenter={false} hides the recenter button in the bottom-right corner.
<InfiniteDragCanvas
className="h-[560px] bg-white dark:bg-neutral-900"
gridSize={24}
momentum={false}
keyboardStep={120}
showRecenter={false}
>
{/* ... */}
</InfiniteDragCanvas>Props
InfiniteDragCanvas
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Put CanvasItem children here |
offset | { x: number; y: number } | — | Controlled pan position, driven by external state |
defaultOffset | { x: number; y: number } | { x: 0, y: 0 } | Initial pan position in uncontrolled mode |
onOffsetChange | (offset) => void | — | Reports pan changes (drag / momentum / recenter / keyboard, with coordinates rounded) |
momentum | boolean | true | Whether to glide with momentum after release |
showGrid | boolean | true | Whether to show the dot-grid background that pans along |
gridSize | number | 40 | Spacing of the dot-grid background (px) |
showRecenter | boolean | true | Whether to show the built-in "back to center" button |
keyboardStep | number | 64 | Distance each arrow-key press pans once focused (px) |
className | string | — | Extra styles (use h-* to override the canvas height) |
CanvasItem
| Prop | Type | Default | Description |
|---|---|---|---|
x | number | — | World-space X (px), with the canvas center as origin |
y | number | — | World-space Y (px), with the canvas center as origin |
children | ReactNode | — | Card content |
className | string | — | Extra styles |
How it works
- Dragging, touch, and momentum gliding are handled by Motion's built-in
dragwith no drag constraints, which is what lets the space extend infinitely. - The dot-grid background pans modulo
gridSize, so covering the viewport plus one cell is enough to create the illusion of infinite repetition; the dot color usescurrentColorand switches with light and dark mode automatically. offset/defaultOffsetsupport controlled and uncontrolled modes: passoffsetand external state drives the pan position; omit it and the component manages it internally, reporting throughonOffsetChange.- The built-in "back to center" button glides smoothly back to
defaultOffset(or the origin) with a spring.
Accessibility
- The canvas is
role="group"and focusable (tabIndex=0), with a descriptivearia-label. - Once focused, arrow keys pan and
Homereturns to center; the arrow keys prevent the page from scrolling. - When the user has "reduce motion" enabled at the system level, momentum gliding is turned off and recentering and keyboard panning jump instantly (with no change to the DOM structure).
- While a drag is in progress,
CanvasItemdisables pointer events so links inside are not triggered by accident and text is not selected.
FAQ
Does it work with the Next.js App Router?
Yes. The component file already starts with "use client", so import it directly from a Server Component page; the content inside each CanvasItem can be rendered on the server too. Only a wrapper that uses useState, like the controlled example, needs to be a Client Component.
How do I change the canvas height?
The default is h-80 with w-full. Pass a height class such as h-[480px] or h-screen through className to override it (classes are merged with cn).
Will lots of cards slow it down?
Panning changes the transform of a single layer through Motion motion values, so in uncontrolled mode a drag does not update React state on every frame. In controlled mode, each onOffsetChange updates your state. The component also does not cull off-screen cards: every CanvasItem stays in the DOM, so keep image sizes in check when you have many.
Does it support zooming?
No. The component provides drag panning, momentum, arrow-key panning and Home to recenter, with no wheel or pinch zoom.
What are the dependencies?
It depends on motion, lucide-react (for the recenter icon), clsx and tailwind-merge. Without the CLI, follow the Manual tab in the installation section to install them and copy the source.
Spotlight Card
A cursor spotlight card — a border glow and a fainter background bloom track the cursor together and fade out on leave, with each card in a row staying independent.
Flip Reveal Card
A 3D flip card that rotates around the X or Y axis to reveal its front and back, supporting hover, click, and fully controlled use.