Cards & Grids

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.

How to install Pro components →See the plans →

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>"

New project or installation trouble? Start here →

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.

40
64
Active edited usageDefault props
<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

PropTypeDefaultDescription
childrenReactNode—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)
momentumbooleantrueWhether to glide with momentum after release
showGridbooleantrueWhether to show the dot-grid background that pans along
gridSizenumber40Spacing of the dot-grid background (px)
showRecenterbooleantrueWhether to show the built-in "back to center" button
keyboardStepnumber64Distance each arrow-key press pans once focused (px)
classNamestring—Extra styles (use h-* to override the canvas height)

CanvasItem

PropTypeDefaultDescription
xnumber—World-space X (px), with the canvas center as origin
ynumber—World-space Y (px), with the canvas center as origin
childrenReactNode—Card content
classNamestring—Extra styles

How it works

  • Dragging, touch, and momentum gliding are handled by Motion's built-in drag with 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 uses currentColor and switches with light and dark mode automatically.
  • offset / defaultOffset support controlled and uncontrolled modes: pass offset and external state drives the pan position; omit it and the component manages it internally, reporting through onOffsetChange.
  • 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 descriptive aria-label.
  • Once focused, arrow keys pan and Home returns 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, CanvasItem disables 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.