Architecture

Gridla is one package with three layers and hard boundaries between them: the core solves layouts as plain data, the interaction layer turns gestures into solver calls without knowing any framework, and each adapter binds that layer to one rendering model.

Core, interaction, adapters

your application       owns the layout data, rendering, persistence, and appearance
        |
        v
adapters               gridla/react   gridla/vue   gridla/svelte   gridla/solid   gridla/angular   gridla/qwik
                       gridla/dom  ->  gridla/elements (Web Components over the DOM adapter)
        |              components, hooks or signals, framework lifecycle
        v
gridla/interaction     createGridController . createPointerGesture . createTransferScope . observeSize
        |              store, projection onto the measured size, gesture API, transfer hit-testing
        v
gridla                 moveItem . resizeItem . placeItem . transferItem . projectLayout . flattenLayout
                       pure functions over plain objects: no DOM, no framework, zero dependencies

gridla/interaction is a public entry. It holds everything the React adapter used to own that was not React: the store, the controller (controlled and uncontrolled sync, projection, onLayoutChange reasons, the gesture API), pointer and keyboard handling over a minimal event shape, the transfer scope with its resting-rect hit-testing, and size observation. Every adapter listed under Adapters is a thin binding over it, so a bug fix or a new gesture lands once and reaches every framework; the Write your own adapter section shows the four functions a new binding needs. The React sections below describe that adapter specifically; the other adapters map the same pieces onto their framework's idioms.

gridla · core (no DOM, no React, zero dependencies)

model

geometry

projection

solver

nested

presets

instrumentation

gridla/react · adapter

provider + store · hooks · components · interaction · transfer scope

the other adapters

DOM, Vue, Svelte, Solid, Angular, Qwik

your application

owns the layout data, rendering, persistence, and appearance

The core is pure functions over plain objects. The React adapter is one consumer of it, the other adapters are its siblings, and your app is another.

Core modules

ModuleResponsibility
modelThe public types (GridLayout, GridItem, GridCanvas, policies, size modes) and shared constants and predicates.
geometryRect math, normalization, bounds, clamping, resizeRect, content extent, validation, gap enforcement. Everything else builds on it.
projectionprojectLayout with the chain and segment engines, plus applyGap.
solvermoveItem, resizeItem, placeItem, transferItem, and the strategy helpers they share.
nestedflattenLayout, tree adapters, hit testing, coordinate conversion, and compactLayout.
presetsapplyPreset for rows, columns, and grids.
instrumentationTraceEvent and SolveStrategy; the optional onTrace channel.

Rules the core follows: every input is a plain object and every output is a new plain object; no function reads or writes anything outside its arguments; no DOM, timers, or globals; results are deterministic for the same inputs. These rules are what make the solvers testable with fixtures and property tests, and what let the same code run in the browser, on the server, and in a worker.

React adapter

The adapter adds exactly what a browser needs: measurement, pointer and keyboard orchestration, state, and headless positioning.

  • Store. A minimal external store (createStore) holds GridState: the source layout, the measured size, the rendered (projected) layout, the gesture in progress, the cursor-tracked rect, the solver preview, selection, and the transferring flag. Components subscribe to slices with useSyncExternalStore, so a drag rerenders only the items whose view changed.
  • Provider. GridProvider creates the store, projects the source onto the measured size, exposes GridActions (programmatic operations that go through the solvers) and an internal gesture API, and reports changes through onLayoutChange and onCommit. Controlled and uncontrolled modes differ only in where the source layout lives.
  • Interaction. useGridInteraction turns pointer and keyboard events on the canvas element into gesture calls. It finds items and handles by data attributes (GRID_DATA), applies the drag threshold, axis lock, and snap bypass, and delegates the math to the core.
  • Components. GridCanvas measures and wires; GridItem positions and exposes handles; GridPreviewOutline draws the landing rect.
  • Transfer scope. GridTransferScope registers providers and, during a drag, previews and commits drops in whichever registered canvas is under the pointer.

Data flow during a drag

pointermove

useGridInteraction

updateMove

moveItem

store.set

GridItem

client px → local px

rendered layout + intent

preview slice

One pointer move: the interaction hook computes a local point, the provider runs the solver on the rendered layout, the store publishes the preview, and subscribed items repaint.
  1. pointerdown on a drag handle selects the item and records a pending press. Once the pointer travels past dragThreshold, beginMove snapshots the item's rect and the grab offset.
  2. Every pointermove converts the client point to canvas-local pixels, applies Shift axis lock, and calls updateMove(pointer, { snap }).
  3. The provider runs moveItem on the rendered layout and stores the result as preview (only when accepted; a rejected step keeps the last accepted preview). activeRect tracks the cursor independently so the dragged item can follow the pointer while siblings show the solved positions.
  4. useGridItemView selects each item's rect from preview.layout (falling back to the rendered layout), and GridItem writes it to transform.
  5. pointerup calls commit: the preview layout becomes the new source (or is handed to onLayoutChange when controlled), onCommit fires, and the gesture state clears. Escape or pointercancel calls cancel instead.

Transfers insert a step between 2 and 3: the scope asks the target provider to previewIncoming, and on release commitIncoming on the target followed by completeOutgoing on the source.

Nested layouts

Nesting is a core concern, not an adapter one. flattenLayout produces root-relative rects and per-container rendered layouts; an application that renders trees keeps one provider per container (or one custom controller over the flat layout) and uses the coordinate conversions in nested to move between them. The studio example is built this way.