Building the spatial canvas
When we started building Hilla, the first technical decision was the canvas. We needed something that could render hundreds of cards with dependency lines, support smooth panning and zooming, and feel instant on every interaction.
Most project tools use a list or a Kanban board. We chose a spatial canvas — and it changed everything about how the product works.
Why spatial?
AI generates plans with structure. Tasks have relationships, dependencies, and groupings that are inherently spatial. A flat list loses this information. A Kanban board forces everything into columns. A canvas preserves the shape of the plan.
When you see 40 tasks arranged spatially with dependency lines connecting them, you immediately understand the project's architecture. You see which tasks are foundational, which are parallel, and which are the critical path.
The rendering challenge
A spatial canvas with hundreds of elements needs to be fast. We explored three approaches:
DOM-based rendering — Each card is a div. Simple to style, but performance degrades quickly past 100 elements. Panning and zooming require transforming every element.
Canvas/WebGL — Maximum performance, but you lose CSS styling, text selection, and accessibility. Every interaction needs custom hit-testing.
Hybrid approach — Use CSS transforms for the viewport, render only visible cards, and virtualize aggressively. This is what we chose.
The implementation
Our canvas uses a single transform on a container element:
container.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
Cards within the container use absolute positioning. We only render cards that intersect the viewport (with a generous buffer), and we batch DOM updates using requestAnimationFrame.
For dependency lines, we use a single SVG overlay that redraws on viewport changes. The lines are calculated from card positions and rendered as cubic bezier paths.
Lessons learned
Debounce viewport calculations, not rendering. We recalculate which cards are visible on every frame, but we debounce the expensive spatial index queries. This keeps panning smooth while avoiding redundant intersection tests.
Use will-change: transform sparingly. It helps with the main container but hurts when applied to every card. The browser creates too many compositing layers.
Pointer events need zoom compensation. When the canvas is zoomed to 0.5x, a click at screen position (100, 100) corresponds to canvas position (200, 200). Every pointer handler needs to account for the current transform.
The canvas is now the core of Hilla. Every feature — from AI plan generation to real-time collaboration — builds on top of it. Getting it right early was worth the investment.