Zero-dependency CSS Grid drag-and-drop. One file.
Drag to rearrange. Resize from corners and edges. Arrow keys to navigate.
| EG Grid | Packery | gridster.js | react-grid-layout | |
|---|---|---|---|---|
| Minified | 28 KB | ~38 KB | ~32 KB | ~43 KB |
| Gzipped | 8.4 KB | ~12 KB | ~10 KB | ~13 KB |
| Dependencies | 0 | 0 (bundled) | jQuery (~30 KB) | React (~42 KB) |
| Total w/ deps | 28 KB | ~48 KB | ~62 KB | ~85 KB+ |
| Framework | none | none | jQuery | React only |
| Layout engine | CSS Grid | abs positioning | abs positioning | CSS transforms |
| Drag | yes | yes (addon) | yes | yes |
| Resize (8-way) | yes | byo | SE only | SE only |
| Keyboard | yes | byo | byo | byo |
| Accessibility | yes (ARIA) | byo | byo | byo |
| View Transitions | yes | byo | byo | byo |
| Vendorable | 1 file | npm/CDN | npm/CDN | npm only |
Frame times measured on a live 8-item grid. All interactions stay under 16ms (60fps budget).
Measured via Chrome DevTools on the grid above. requestAnimationFrame timing, 8 items, push algorithm.
// Core library (8.4 KB gzip) curl -fsSL https://raw.githubusercontent.com/derekr/eg-grid/main/dist/eg-grid.js -o lib/eg-grid.js // Web component (10.5 KB gzip) curl -fsSL https://raw.githubusercontent.com/derekr/eg-grid/main/dist/eg-grid-element.js -o lib/eg-grid-element.js
Or run the vendor script — picks a bundle, configures your grid, outputs starter HTML and CSS:
curl -fsSL https://raw.githubusercontent.com/derekr/eg-grid/main/vendor.sh | bash
<style id="egg-styles">
[data-egg-item="a"] { grid-column: 1 / span 2; grid-row: 1; }
[data-egg-item="b"] { grid-column: 3; grid-row: 1; }
[data-egg-item="c"] { grid-column: 1; grid-row: 2; }
</style>
<div class="grid" id="grid">
<div data-egg-item="a">A</div>
<div data-egg-item="b">B</div>
<div data-egg-item="c">C</div>
</div>
<script type="module">
import { init } from './lib/eg-grid.js';
const core = init(document.getElementById('grid'), {
algorithm: 'push',
styleElement: document.getElementById('egg-styles'),
resize: { handles: 'all' },
});
</script>
<script type="module" src="lib/eg-grid-element.js"></script>
<style>
[data-egg-item="a"] { grid-column: 1 / span 2; grid-row: 1; }
[data-egg-item="b"] { grid-column: 3; grid-row: 1; }
[data-egg-item="c"] { grid-column: 1; grid-row: 2; }
</style>
<eg-grid columns="4" gap="8" algorithm="push" resize-handles="all">
<div data-egg-item="a">A</div>
<div data-egg-item="b">B</div>
<div data-egg-item="c">C</div>
</eg-grid>
CSS Grid does the layout. JavaScript only sets grid-column and grid-row
via <style> injection. The browser computes all pixel positions, handles animations
via View Transitions, and manages responsive behavior through container queries.
During drag, the item leaves grid flow (position: fixed) so the grid can reflow around it.
On drop, it rejoins the grid at its new position.
State machine: idle → selected → interacting → idle.
Three phases, tracked on core.phase.
Everything is in one file. No plugins to configure — just init() options.
| Option | Default | Description |
|---|---|---|
algorithm | false | 'push' or false |
resize | enabled | { handles: 'corners' | 'edges' | 'all' } or false |
camera | enabled | Auto-scroll during drag. false to disable |
placeholder | enabled | Drop target indicator. { className } or false |
keyboard | enabled | Arrow key navigation. false to disable |
accessibility | enabled | ARIA announcements. false to disable |
pointer | enabled | Mouse/touch drag. false to disable |
responsive | disabled | { layoutModel, cellSize, gap } |
styleElement | auto-created | <style> element for CSS injection |
const core = init(element, options);
core.phase // 'idle' | 'selected' | 'interacting'
core.interaction // { type, mode, itemId, element, columnCount } | null
core.selectedItem // HTMLElement | null
core.select(item) // Programmatic select
core.deselect() // Programmatic deselect
core.baseCSS // Get/set base layout CSS
core.previewCSS // Get/set preview CSS (during drag/resize)
core.commitStyles() // Flush CSS to <style> element
core.destroy() // Clean up all listeners
All events bubble from the grid element. source is 'pointer' or 'keyboard'.
| Event | Detail |
|---|---|
egg-drag-start | { item, cell, colspan, rowspan, source } |
egg-drag-move | { item, cell, colspan, rowspan, source } |
egg-drag-end | { item, cell, colspan, rowspan, source } |
egg-drag-cancel | { item, source } |
egg-resize-start | { item, handle, colspan, rowspan, source } |
egg-resize-move | { item, handle, colspan, rowspan, source } |
egg-resize-end | { item, handle, colspan, rowspan, source } |
egg-resize-cancel | { item, source } |
egg-select | { item } |
egg-deselect | { item } |
egg-column-count-change | { columnCount } |
| Key | Action |
|---|---|
| Arrow keys / hjkl | Nudge item by 1 cell |
| Shift+Arrow | Resize item |
| Ctrl+Arrow | Jump by item size |
| Alt+Arrow | Select adjacent item |
| Enter / Space | Pick up / drop item |
| Escape | Cancel or deselect |
| Shift+G | Toggle keyboard mode |
Items are styled via CSS attribute selectors. The library sets data attributes automatically:
[data-egg-dragging] { cursor: grabbing; z-index: 100; }
[data-egg-dropping] { z-index: 100; }
[data-egg-selected] { outline: 2px solid #fbbf24; z-index: 10; }
[data-egg-resizing] { z-index: 100; }
[data-egg-handle-hover="se"]::after { /* show SE handle */ }
[data-egg-keyboard-mode] { outline: 2px solid gold; }
For animated layout changes, give items a view-transition-name:
.item { view-transition-name: var(--item-id); }
::view-transition-group(*) {
animation-duration: 200ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
| Attribute | Purpose |
|---|---|
data-egg-item | Mark as grid item (value = item ID) |
data-egg-colspan | Column span (default: 1, or derived from CSS) |
data-egg-rowspan | Row span (default: 1, or derived from CSS) |
data-egg-label | Human-readable name for accessibility |
This is not a package you install. Copy one file into your codebase and own it.
src/ eg-grid.ts ← THE library (1,165 lines, everything in one file) eg-grid-element.ts ← <eg-grid> web component wrapper layout-model.ts ← Responsive layout model (optional) bundles/element.ts ← Web component bundle entry plugins/dev-overlay.ts← Debug panel (Shift+D, optional)
| Bundle | Gzip | Description |
|---|---|---|
eg-grid.js | 8.4 KB | Core library |
eg-grid-element.js | 10.5 KB | Web component |
dev-overlay.js | 3.1 KB | Debug panel (Shift+D) |