Exploring Infinite Canvas: My Journey & Insights
I’ve always been fascinated by infinite canvas interfaces - those smooth, zoomable, pannable spaces that apps like Figma and Miro use. So I decided to reverse-engineer one. Turns out, you only need 2 HTML elements to make it work.
The Core Concept
At its heart, an infinite canvas is surprisingly simple:
- A viewport (the window you look through)
- A canvas (the surface that moves)
That’s it. Everything else is just math and event handling.
The Implementation
Here’s the minimal setup that got me started:
<main id="infinite-canvas" style={{ width: "100dvw", height: "100dvh" }}>
<div
id="object-to-move"
style={{
position: "absolute",
transform: `scale(${scale}) translate(${x}px, ${y}px)`
}}
>
Scroll to move or pinch to zoom
</div>
</main>
The magic happens in that transform
property. By combining scale()
and translate()
, you get smooth zooming and panning with hardware acceleration.
Handling Gestures
The tricky part was making it feel natural. I experimented with a few approaches:
Approach 1: Raw Event Listeners
Started with basic wheel
and pointermove
events. Works, but the math for pinch-to-zoom gets complex quickly.
Approach 2: @use-gesture/react
This library from the Poimandres collective is fantastic. It handles all the edge cases:
import { useGesture } from '@use-gesture/react'
const bind = useGesture({
onWheel: ({ delta: [dx, dy] }) => {
setPosition([x - dx, y - dy])
},
onPinch: ({ offset: [scale] }) => {
setScale(scale)
}
})
The Gotchas I Hit
-
Transform Origin: Always transform from the cursor position, not the center. Users expect to zoom into what they’re looking at.
-
Bounds Calculation: When you’re zoomed in, the coordinate system gets weird. You need to account for scale when calculating boundaries.
-
Performance: DOM updates are expensive. Use
transform
instead of changingtop/left
. Consider usingwill-change: transform
for smoother animations.
What I Learned
The biggest insight? Most “complex” UI patterns are simpler than they appear. The trick is understanding the core mechanics, then layering on the polish.
I spent a week thinking I needed a complex state machine, collision detection, and a rendering pipeline. Turns out I just needed CSS transforms and some event math.
Next Steps
Now that I have the basics working, I want to add:
- Momentum scrolling for that iOS-like feel
- Minimap navigation
- Snap-to-grid functionality
- Multi-touch gesture support
The code is surprisingly portable too. Once you understand the transform math, you can implement this in any framework - React, Vue, vanilla JS, whatever.
Sometimes the best way to learn is to just open DevTools on your favorite app and see how they did it. You’d be surprised how much you can figure out just by inspecting the DOM and watching the styles change.