Skip to content

illm4tic/vue-gridstack

Repository files navigation

vue-gridstack logo

vue-gridstack

A Vue 3 controlled grid layout component built on Gridstack.js for draggable, resizable, responsive dashboards and workbenches.

English | 简体中文

npm version npm downloads MIT license Vue 3.5+ Gridstack 12.3+

Links

Why vue-gridstack?

vue-gridstack wraps Gridstack.js with a Vue-first controlled data model. Your application owns the layout through v-model:layout, while GridstackItem keeps Vue slot content stable and lets Gridstack handle drag, resize, collision, and responsive behavior.

This keeps dashboard state easy to persist, restore, diff, and synchronize with your own data model.

Features

  • Vue 3 component API with named exports and plugin installation.
  • Controlled layout through v-model:layout.
  • Stable slot rendering for arbitrary Vue components inside widgets.
  • Drag, resize, static, locked, min/max size, auto-position, and sub-grid options.
  • Built-in heightMode="fill" for fixed-height dashboards, video walls, and control rooms.
  • Gridstack event forwarding with the calculated next layout.
  • Typed public API and exported TypeScript types.
  • Access to the underlying GridStack instance for advanced use cases.

Installation

Install the Vue wrapper:

npm install vue-gridstack
pnpm add vue-gridstack
yarn add vue-gridstack

vue-gridstack imports the required component styles and Gridstack styles from its entry. If your bundler needs an explicit style entry, import it once in your app:

import "vue-gridstack/style.css";

Runtime dependencies:

Package Version
gridstack Bundled with vue-gridstack

Peer dependencies:

Package Version
vue ^3.5.0

Quick Start

<template>
  <Gridstack v-model:layout="layout" :options="gridOptions">
    <GridstackItem id="sales">
      <SalesCard />
    </GridstackItem>

    <GridstackItem id="traffic">
      <TrafficChart />
    </GridstackItem>
  </Gridstack>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Gridstack, GridstackItem } from "vue-gridstack";
import type { GridstackLayoutItem, GridstackOptions } from "vue-gridstack";

const gridOptions = ref<GridstackOptions>({
  column: 12,
  margin: 8,
  disableDrag: false,
  disableResize: false
});

const layout = ref<GridstackLayoutItem[]>([
  { id: "sales", x: 0, y: 0, w: 4, h: 3 },
  { id: "traffic", x: 4, y: 0, w: 8, h: 3 }
]);
</script>

Global registration is also supported:

import { createApp } from "vue";
import VueGridstack from "vue-gridstack";
import App from "./App.vue";

createApp(App).use(VueGridstack).mount("#app");

Core Model

Use this pattern for new code:

  • Gridstack receives container-level options such as column, margin, staticGrid, disableDrag, and disableResize.
  • Gridstack owns widget geometry through v-model:layout.
  • GridstackItem only receives a stable id and renders your Vue slot content.
  • Each GridstackItem id must match an item in layout.
<Gridstack v-model:layout="layout" :options="{ column: 12 }">
  <GridstackItem v-for="widget in widgets" :key="widget.id" :id="widget.id">
    <WidgetCard :widget="widget" />
  </GridstackItem>
</Gridstack>
const layout = ref<GridstackLayoutItem[]>([
  { id: "cpu", x: 0, y: 0, w: 3, h: 2 },
  { id: "memory", x: 3, y: 0, w: 3, h: 2, minW: 2 },
  { id: "logs", w: 6, h: 3, autoPosition: true }
]);

API

Gridstack Props

Prop Type Default Description
layout GridstackLayoutItem[] [] Controlled widget layout. Prefer v-model:layout.
options GridstackOptions {} Container-level Gridstack options passed to GridStack.init() and grid.updateOptions().
heightMode "native" | "fill" "native" Controls how the component handles grid height and cellHeight.

GridstackItem Props

Prop Type Default Description
id string | number Required Stable widget id. Must match the corresponding layout item.

Do not pass geometry props such as x, y, w, h, noResize, or noMove to GridstackItem. Put widget configuration in layout.

<!-- Recommended -->
<Gridstack v-model:layout="layout">
  <GridstackItem id="chart">
    <ChartPanel />
  </GridstackItem>
</Gridstack>
const layout = ref([{ id: "chart", x: 0, y: 0, w: 4, h: 3 }]);

Layout Item Fields

Every item needs a stable id. The component forwards the following widget fields to Gridstack:

Field Type Description
id string | number Unique widget id, normalized to string internally.
x number | string Column position, starting at 0.
y number | string Row position, starting at 0.
w number | string Width in columns. Normalized to at least 1.
h number | string Height in rows. Normalized to at least 1.
autoPosition boolean | string Lets Gridstack place the widget automatically. When true, x and y are ignored until placement is committed.
minW / maxW number | string Minimum and maximum width.
minH / maxH number | string Minimum and maximum height.
noResize boolean | string Prevents this widget from being resized.
noMove boolean | string Prevents this widget from being dragged.
locked boolean | string Locks this widget according to Gridstack behavior.
content string Native Gridstack content field. Slot content is recommended for Vue.
sizeToContent boolean | number | string Forwards Gridstack size-to-content behavior.
lazyLoad boolean | string Native Gridstack lazy-load field.
resizeToContentParent string Parent selector used by Gridstack content resizing.
subGridOpts object Native Gridstack sub-grid options.

Layout update rules:

  • Parent changes to layout are synchronized into Gridstack.
  • Drag and resize changes emit update:layout.
  • Existing business fields on layout items are preserved when the component writes back x, y, w, and h.
  • autoPosition: true is removed after Gridstack resolves the final position.
  • Fields not listed above can live in layout, but they are not passed to Gridstack.

options

options accepts native Gridstack container options. The component adds one default:

{
  auto: false
}

auto: false prevents Gridstack from scanning DOM before Vue slot content is ready. Widgets are created and updated by the component based on layout and rendered GridstackItem nodes.

Common options:

const gridOptions = computed<GridstackOptions>(() => ({
  column: 12,
  margin: 8,
  float: false,
  animate: true,
  disableDrag: !editing.value,
  disableResize: !editing.value,
  draggable: {
    handle: ".grid-stack-item-content",
    appendTo: "body"
  },
  resizable: {
    handles: "all",
    autoHide: true
  }
}));

heightMode

heightMode="native"

The default mode. Grid height follows native Gridstack behavior and your options.cellHeight.

<Gridstack v-model:layout="layout" height-mode="native" :options="{ column: 12, cellHeight: 80 }">
  <GridstackItem id="a">
    <Widget />
  </GridstackItem>
</Gridstack>

Use this mode for normal page flow, long dashboards, and layouts that should grow naturally.

heightMode="fill"

The component fills its parent height and calculates cellHeight from the parent height, row count, and margin.

<template>
  <section class="dashboard">
    <Gridstack v-model:layout="layout" height-mode="fill" :options="{ row: 6, column: 12 }">
      <GridstackItem id="camera">
        <CameraPlayer />
      </GridstackItem>
    </Gridstack>
  </section>
</template>

<style scoped>
.dashboard {
  height: 100%;
  min-height: 560px;
}
</style>

Use this mode for fixed canvases, video walls, monitoring screens, and workbenches that must fill a known container. The parent element must have an explicit height. Setting options.row or options.minRow is recommended.

Events

Gridstack forwards common Gridstack events. The change event also includes the computed nextLayout.

Event Payload Description
ready (grid) Gridstack instance is initialized.
change (items, event, grid, nextLayout) Layout changed. nextLayout is the computed controlled layout.
added (items, event, grid) Widgets were added.
removed (items, event, grid) Widgets were removed.
dragstart (element, event, grid) Drag started.
drag (element, event, grid) Dragging.
dragstop (element, event, grid) Drag stopped.
resizestart (element, event, grid) Resize started.
resize (element, event, grid) Resizing.
resizestop (element, event, grid) Resize stopped.
resizecontent (items, event, grid) Gridstack content resize event.
dropped (previousNode, newNode, event, grid) External item was dropped into the grid.
enable (event, grid) Gridstack was enabled.
disable (event, grid) Gridstack was disabled.
<Gridstack v-model:layout="layout" @ready="onReady" @change="onChange">
  <GridstackItem id="a">
    <Widget />
  </GridstackItem>
</Gridstack>
function onReady(grid: GridStack) {
  grid.compact();
}

function onChange(items, event, grid, nextLayout) {
  console.log(nextLayout);
}

You usually do not need to assign layout manually in change; v-model:layout already receives updates. Use change for persistence, analytics, or side effects.

Exposed Methods

Use a template ref to access the public methods:

<template>
  <Gridstack ref="gridRef" v-model:layout="layout">
    <GridstackItem id="a">
      <Widget />
    </GridstackItem>
  </Gridstack>
</template>

<script setup lang="ts">
import { ref } from "vue";
import type { GridstackExpose } from "vue-gridstack";

const gridRef = ref<GridstackExpose | null>(null);

function saveCurrentLayout() {
  const nextLayout = gridRef.value?.getLayout() ?? [];
  return api.saveLayout(nextLayout);
}

function compact() {
  gridRef.value?.getGridStack()?.compact();
}
</script>
Method Returns Description
getGridStack() GridStack | null Returns the native Gridstack instance.
getGridElement() HTMLElement | null Returns the inner .grid-stack-inner element.
getShellElement() HTMLElement | null Returns the outer shell element.
syncWidgets() void Manually synchronizes rendered widgets with Gridstack.
updateGridCellHeight() void Recalculates cellHeight in heightMode="fill".
getLayout() GridstackLayoutItem[] Reads the current layout from Gridstack.
getOptions() GridstackOptions Returns the resolved Gridstack options used by the component.

Recipes

Read-only Dashboard

<Gridstack v-model:layout="layout" :options="{ column: 12, staticGrid: true }">
  <GridstackItem v-for="item in widgets" :key="item.id" :id="item.id">
    <WidgetCard :data="item" />
  </GridstackItem>
</Gridstack>

You can also use disableDrag and disableResize:

<Gridstack
  v-model:layout="layout"
  :options="{ column: 12, disableDrag: true, disableResize: true }"
>
  <GridstackItem id="a">
    <Widget />
  </GridstackItem>
</Gridstack>

Editable Layout

<template>
  <button type="button" @click="editing = !editing">Toggle edit</button>

  <Gridstack v-model:layout="layout" :options="gridOptions" @change="dirty = true">
    <GridstackItem v-for="item in widgets" :key="item.id" :id="item.id">
      <WidgetCard :data="item" />
    </GridstackItem>
  </Gridstack>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import type { GridstackOptions } from "vue-gridstack";

const editing = ref(false);
const dirty = ref(false);

const gridOptions = computed<GridstackOptions>(() => ({
  column: 12,
  margin: 8,
  disableDrag: !editing.value,
  disableResize: !editing.value,
  resizable: { handles: "all" }
}));
</script>

Add Widgets Dynamically

Add both the business item and its layout item. Use autoPosition: true when you want Gridstack to find a place.

function addWidget() {
  const id = `widget-${Date.now()}`;

  widgets.value.push({
    id,
    title: "New widget"
  });

  layout.value.push({
    id,
    w: 3,
    h: 2,
    autoPosition: true
  });
}

After Gridstack places the widget, the component writes back the real x, y, w, and h.

Remove Widgets

Keep your rendered list and layout in sync:

function removeWidget(id: string) {
  widgets.value = widgets.value.filter(item => item.id !== id);
  layout.value = layout.value.filter(item => String(item.id) !== id);
}

Lock a Single Widget

const layout = ref<GridstackLayoutItem[]>([
  {
    id: "fixed-panel",
    x: 0,
    y: 0,
    w: 4,
    h: 3,
    noMove: true,
    noResize: true
  }
]);

Fill a Parent Container

<template>
  <section class="video-wall">
    <Gridstack v-model:layout="layout" height-mode="fill" :options="gridOptions">
      <GridstackItem v-for="camera in cameras" :key="camera.id" :id="camera.id">
        <CameraPlayer :camera="camera" />
      </GridstackItem>
    </Gridstack>
  </section>
</template>

<script setup lang="ts">
import { ref } from "vue";
import type { GridstackOptions } from "vue-gridstack";

const gridOptions = ref<GridstackOptions>({
  row: 2,
  column: 4,
  margin: 6,
  float: true,
  disableDrag: true,
  disableResize: true
});
</script>

<style scoped>
.video-wall {
  height: 100%;
  min-height: 560px;
}
</style>

Use Gridstack Advanced APIs

<Gridstack ref="gridRef" v-model:layout="layout" @ready="onReady">
  <GridstackItem id="a">
    <Widget />
  </GridstackItem>
</Gridstack>
function onReady(grid: GridStack) {
  grid.compact();
}

function batchUpdate() {
  const grid = gridRef.value?.getGridStack();
  if (!grid) return;

  grid.batchUpdate();
  // call native Gridstack APIs here
  grid.batchUpdate(false);

  layout.value = gridRef.value?.getLayout() ?? layout.value;
}

Native DOM Compatibility

GridstackItem is recommended for Vue apps. For migration from native Gridstack markup, direct DOM nodes are also supported:

<Gridstack :options="{ column: 12 }">
  <div class="grid-stack-item" gs-id="native-a" gs-x="0" gs-y="0" gs-w="4" gs-h="2">
    <div class="grid-stack-item-content">Native A</div>
  </div>

  <div class="grid-stack-item" gs-id="native-b" gs-w="3" gs-h="2" gs-auto-position="true">
    <div class="grid-stack-item-content">Native B</div>
  </div>
</Gridstack>

The component can read these attributes: gs-id, gs-x, gs-y, gs-w, gs-h, gs-min-w, gs-max-w, gs-min-h, gs-max-h, gs-auto-position, gs-no-resize, gs-no-move, gs-locked, and gs-size-to-content.

TypeScript Exports

import type {
  GridstackExpose,
  GridstackHeightMode,
  GridstackItemId,
  GridstackLayoutItem,
  GridstackOptions
} from "vue-gridstack";

Development

npm install
npm run dev
npm run typecheck
npm run build
npm run build:demo
npm run build:all
npm run pack:check

Script summary:

Script Description
npm run dev Starts the local demo app.
npm run typecheck Runs Vue and TypeScript type checking.
npm run build Builds the npm library output into dist.
npm run build:demo Builds the deployable demo into dist-demo.
npm run build:all Builds both library and demo.
npm run pack:check Checks the npm package contents with a dry run.

Troubleshooting

The widget appears in the DOM but is not where I expect

Make sure the GridstackItem id has a matching layout item. If no layout item exists, the component creates a fallback { id, w: 1, h: 1, autoPosition: true }.

The fill layout has incorrect height

heightMode="fill" requires the parent element to have a real height. Add height, min-height, or a layout rule that gives the parent a stable height.

Vue content is replaced unexpectedly

Render Vue content with slots instead of the native Gridstack content field.

Layout persistence misses custom data

Custom fields on existing layout items are preserved during write-back, but only supported Gridstack widget fields are forwarded to Gridstack.

License

MIT

About

A generic Vue3 wrapper for gridstack.js with controlled layouts, draggable and resizable widgets, auto-positioning, and fill-height dashboard support.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors