Skip to Content
New — snapgrid now ships for Svelte 5: the same dnd-kit-native, headless-first grid — drag, resize, repack, and drag between grids. See it in Svelte →
VueDocumentationGuidesHeadless usage

Headless usage

The composables are snapgrid’s primary API. They hand you function refs, positioning styles, and drag state — you bring the markup, classes, and content. Reach for them when you want full control, or to compose with the dnd-kit draggables and sortables you already have (dnd-kit interop). (<GridLayout> is a thin shell over these same composables — see Component layer when you’d rather not wire it yourself.)

Drag & resize
drag a tile · resize from the corner

The shape of a headless grid

Two pieces, both under a dnd-kit DragDropProvider that you supply:

  1. useGridContainer(() => options) — the grid host. Returns a setRef function ref for your surface and the grid’s group.
  2. useGridItem({ id, group }) — one tile. Returns a setRef function ref and positioning style.

There’s no third piece for the drag preview: a dragged tile floats itself.

<!-- Board.vue --> <script setup> import { DragDropProvider } from "@snapgridjs/vue"; import Surface from "./Surface.vue"; defineProps({ layout: Array, width: Number }); const emit = defineEmits(["layoutChange"]); </script> <template> <DragDropProvider> <Surface :layout="layout" :width="width" @layout-change="emit('layoutChange', $event)" /> </DragDropProvider> </template>
<!-- Surface.vue --> <script setup> import { useGridContainer } from "@snapgridjs/vue"; import Tile from "./Tile.vue"; const props = defineProps({ layout: Array, width: Number }); const emit = defineEmits(["layoutChange"]); const { setRef, style, group } = useGridContainer(() => ({ layout: props.layout, width: props.width, onLayoutChange: (next) => emit("layoutChange", next), })); </script> <template> <div :ref="setRef" :style="style"> <Tile v-for="it in layout" :key="it.i" :id="it.i" :group="group" /> </div> </template>
<!-- Tile.vue --> <script setup> import { useGridItem } from "@snapgridjs/vue"; const props = defineProps({ id: String, group: String }); const { setRef, style } = useGridItem({ id: props.id, group: props.group }); </script> <template> <div :ref="setRef" :style="style" class="tile">{{ id }}</div> </template>

useGridContainer must run inside the provider. It registers the grid on dnd-kit’s manager, and Vue resolves inject from the parent chain — so the host has to be a child component rendered inside <DragDropProvider> (like Surface), not the component that renders the provider. Calling it in the same component that renders <DragDropProvider> leaves the manager un-injectable, and tiles throw no grid found for group. (SnapGridGroup is dnd-kit’s provider too, so the same rule applies.)

useGridContainer — the grid host

useGridContainer(() => options) creates the grid’s controller + drag monitor and returns:

FieldWhat it is
setRefFunction ref for your surface element: <div :ref="setRef">.
styleReactive inline-style string (position: relative + width + auto-sized height). Bind it: :style="style" — don’t spread it.
groupThis grid’s id. Pass it to every tile-level composable.
isDropTargettrue while a compatible draggable is over the grid (handy for highlighting).
controllerThe grid’s controller, for advanced composition.

Every field except setRef and controller is a ref. Destructure the result, as above — Vue only auto-unwraps top-level <script setup> bindings, so container.style in a template would render [object Object] while style (destructured) unwraps to the string. In plain script, read group.value.

options is the controlled state — layout, width, onLayoutChange — plus any of gridConfig, dragConfig, resizeConfig, dropConfig, compactor, isDraggable, isResizable, autoSize, id, and the lifecycle callbacks. They’re passed as a getteruseGridContainer(() => ({ … })) — so the grid always reads their latest values. Each is documented in its feature guide.

useGridItem — a tile

useGridItem({ id, group }) wires one tile. Unlike the container, its options are plain values, not a getter: a tile’s id and group are read once at setup and are expected to be stable for its lifetime.

FieldWhat it is
setRefFunction ref for the tile element.
setHandleRefOptional drag handle — bind it to a child to restrict pointer dragging to it (see Dragging). Leave it off and the whole tile drags.
styleAbsolute-positioning string (position: absolute + left/top/width/height); reflow is animated on the compositor. Bind it with :style="style"; don’t override the position.
isDraggingtrue while this tile is the active drag source — branch your styling on it.
itemThe tile’s current (possibly reflowed) layout entry.

You choose the tag, classes, and content. snapgrid only supplies position + state.

The dragged tile floats itself

There’s no overlay to render. While you drag with a pointer, dnd-kit lifts the active tile into the browser’s top layer, so it floats above everything and crosses between grids unclipped — the same element you rendered, now floating. The other tiles reflow to open the landing cell, and on drop the tile settles into place. This is dnd-kit’s default drag feedback; snapgrid only disables the drop animation so the tile lands on the cell instead of springing back to where it started.

Because the tile is the preview, you style its dragging state right where you render it — branch on isDragging:

<!-- Tile.vue --> <script setup> import { useGridItem } from "@snapgridjs/vue"; const props = defineProps({ id: String, group: String }); const { setRef, style, isDragging } = useGridItem({ id: props.id, group: props.group }); </script> <template> <div :ref="setRef" :style="style" class="tile" :data-dragging="isDragging || undefined" > {{ id }} </div> </template>

Keyboard drags don’t float. With the keyboard sensor the tile stays in place and steps cell-by-cell (Enter/Space to pick up and drop, arrows to move, Escape to cancel) — so the floating behaviour is purely for pointer drags.

Need a separate floating element? For a custom preview — a clone, a ghost, different markup — dnd-kit’s DragOverlay is re-exported from @snapgridjs/vue. You rarely need it, since the tile already floats itself; reach for it only when the floating preview must differ from the tile. See Styling.

Optional affordances

Headless tiles carry nothing extra — no class names, no resize handles, no placeholder, just a setRef and a style. The host and tile above are already a complete, working grid; everything else is yours to render. Two affordances are common enough that snapgrid gives you a composable for each:

  1. useGridResizeHandle({ id, handle, group }) — render resize handles on a tile.
  2. useGridPlaceholder(group) — mark the cell a dragged tile will land in.

useGridResizeHandle — render your own resize handles

Why you need it: a headless tile has no resize handles. resizeConfig.handles only declares which edges are resizable; you render the handle elements. (The <GridItem> shell does this for you — this is what it does under the hood.)

useGridResizeHandle({ id, handle, group }) models a handle as its own draggable and returns:

FieldWhat it is
setRefFunction ref for your handle element.
handlePropsSpread onto the handle with v-bind — marks it so a pointer-down resizes instead of starting an item drag.
isResizingtrue while this item is being resized.

You position and style the handle however you like:

<!-- Tile.vue --> <script setup> import { useGridItem, useGridResizeHandle } from "@snapgridjs/vue"; const props = defineProps({ id: String, group: String }); const { setRef, style } = useGridItem({ id: props.id, group: props.group }); const { setRef: setResizeRef, handleProps } = useGridResizeHandle({ id: props.id, handle: "se", group: props.group, }); </script> <template> <div :ref="setRef" :style="style" class="tile"> {{ id }} <!-- a bottom-right grip; CSS places + styles `.resize-handle` --> <span :ref="setResizeRef" v-bind="handleProps" class="resize-handle" /> </div> </template>

Render one handle per axis you want ("n" | "e" | "s" | "w" | "ne" | "nw" | "se" | "sw"). See Resizing for the config and per-item limits.

useGridPlaceholder — the landing marker

Why you’d use it: during a drag, the placeholder shows the cell the tile will land in — the gap that reflows live as you move. It’s optional; render it for the familiar “ghost cell” affordance.

useGridPlaceholder(group) returns a computed ref holding a { item, style } object while a drag is active, or null when idle. Read it in the template (Vue unwraps it) and bind its style:

<!-- Surface.vue --> <script setup> import { useGridContainer, useGridPlaceholder } from "@snapgridjs/vue"; import Tile from "./Tile.vue"; const props = defineProps({ layout: Array, width: Number }); const emit = defineEmits(["layoutChange"]); const { setRef, style, group } = useGridContainer(() => ({ layout: props.layout, width: props.width, onLayoutChange: (next) => emit("layoutChange", next), })); // `group` is a ref, and the grid's id is stable for its lifetime — read it once here. const placeholder = useGridPlaceholder(group.value); </script> <template> <div :ref="setRef" :style="style"> <Tile v-for="it in layout" :key="it.i" :id="it.i" :group="group" /> <div v-if="placeholder" class="placeholder" :style="placeholder.style" /> </div> </template>

Fine-grained updates — no memo needed

The surface’s auto-height tracks the drag, but Vue’s reactivity is fine-grained: each useGridItem reads only its own slice of the controller through computeds, so only the tile whose cell actually changed re-renders. There’s no React-style memo to reach for — the composables already subscribe narrowly, and a neighbour whose cell didn’t move stays put. (<GridItem> works the same way.)

Mixing the two layers

They share one engine, so nothing is all-or-nothing. Drop <GridItem :id :group> / <GridPlaceholder :group> into a surface you host with useGridContainer, or use <GridLayout> for most of the app and the composables for the one grid that needs custom markup. See Component layer.

The composables at a glance

ComposableReturnsRenders
useContainerWidth(options?){ width, mounted, setRef }Nothing — measures an element.
useGridContainer(() => options){ setRef, style, group, isDropTarget, controller }The surface; registers the droppable + owns the grid.
useGridItem({ id, group }){ setRef, setHandleRef, style, isDragging, item }One positioned tile.
useGridResizeHandle({ id, handle, group }){ setRef, handleProps, isResizing }One resize handle.
useGridPlaceholder(group)ComputedRef<{ item, style } | null>The landing-cell marker (null when idle).
useResponsiveLayout(() => options){ breakpoint, cols, layout, onLayoutChange }Nothing — resolves the active breakpoint. See Responsive.
Last updated on