UI Components Medium
Draggable Bento Rearrange
Rearrange dashboard tiles with native drag events while preserving a responsive CSS Grid.
Open in Lab
MCP
html css javascript react
Targets: TS JS HTML React
Code
:root {
--bg: #0a0b0f;
--panel: #14161d;
--panel-2: #191c25;
--line: #262a36;
--text: #e9ecf4;
--muted: #8d95a8;
--accent: #7c9cff;
--up: #4ade80;
--down: #fb7185;
--radius: 16px;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(1000px 600px at 20% -10%, #171a24, var(--bg));
color: var(--text);
font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.demo {
width: min(940px, 100%);
margin: 0 auto;
padding: clamp(1.25rem, 4vw, 2.75rem);
}
.demo__head h1 { margin: 0 0 .4rem; font-size: 1.5rem; letter-spacing: -.02em; }
.demo__head p {
margin: 0 0 1.5rem;
color: var(--muted);
font-size: .875rem;
max-width: 62ch;
}
kbd {
background: var(--panel-2);
border: 1px solid var(--line);
border-bottom-width: 2px;
border-radius: 5px;
padding: .05em .4em;
font: inherit;
font-size: .8em;
color: var(--text);
}
.bento {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-auto-rows: 118px;
gap: 14px;
}
.tile {
position: relative;
display: flex;
flex-direction: column;
gap: .35rem;
padding: 1rem;
background: linear-gradient(160deg, var(--panel-2), var(--panel));
border: 1px solid var(--line);
border-radius: var(--radius);
cursor: grab;
user-select: none;
transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease, opacity .18s ease;
}
.tile:hover { border-color: #333a4b; transform: translateY(-2px); }
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.tile:active { cursor: grabbing; }
.tile--wide { grid-column: span 2; }
.tile--tall { grid-row: span 2; }
.tile__kicker {
font-size: .72rem;
letter-spacing: .09em;
text-transform: uppercase;
color: var(--muted);
}
.tile__value {
font-size: 1.6rem;
font-weight: 650;
letter-spacing: -.03em;
font-variant-numeric: tabular-nums;
}
.tile__delta { font-size: .8rem; font-weight: 550; }
.tile__delta--up { color: var(--up); }
.tile__delta--down { color: var(--down); }
.tile__spark {
margin-top: auto;
height: 46px;
border-radius: 8px;
background:
linear-gradient(to top, rgba(124, 156, 255, .28), transparent),
repeating-linear-gradient(90deg, rgba(124, 156, 255, .55) 0 3px, transparent 3px 14px);
-webkit-mask-image: linear-gradient(to top, #000 65%, transparent);
mask-image: linear-gradient(to top, #000 65%, transparent);
}
.tile.is-dragging { opacity: .3; border-style: dashed; }
.tile.is-over {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 12px 30px -12px rgba(124, 156, 255, .6);
}
.tile.is-lifted {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 18px 40px -18px rgba(124, 156, 255, .7);
transform: scale(1.02);
}
.demo__status {
margin-top: 1.5rem;
font-size: .8rem;
color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (max-width: 600px) {
.tile--wide { grid-column: span 1; }
}
@media (prefers-reduced-motion: reduce) {
.tile { transition: none; }
.tile:hover, .tile.is-lifted { transform: none; }
}/**
* Draggable Bento Rearrange
* - Native HTML5 drag & drop reordering inside a CSS Grid.
* - Order lives in a JS array (the "state"); the DOM is re-synced from it.
* - Full keyboard fallback: Space to lift, arrows to move, Space to drop, Esc to cancel.
*/
(function () {
const grid = document.getElementById("grid");
const status = document.getElementById("status");
if (!grid) return;
/** @type {string[]} source of truth */
let order = [...grid.querySelectorAll(".tile")].map((t) => t.dataset.id);
let orderBeforeLift = null;
const tileById = (id) => grid.querySelector(`.tile[data-id="${id}"]`);
function render() {
// Reorder DOM to match state without destroying nodes (keeps focus alive).
order.forEach((id) => grid.appendChild(tileById(id)));
status.textContent = "Order: " + order.join(", ");
}
function move(id, toIndex) {
const from = order.indexOf(id);
if (from === -1) return;
const to = Math.max(0, Math.min(order.length - 1, toIndex));
if (to === from) return;
order.splice(from, 1);
order.splice(to, 0, id);
render();
}
/* ---------- pointer drag ---------- */
let draggingId = null;
grid.addEventListener("dragstart", (e) => {
const tile = e.target.closest(".tile");
if (!tile) return;
draggingId = tile.dataset.id;
e.dataTransfer.effectAllowed = "move";
// Some browsers need payload for the drag to start.
e.dataTransfer.setData("text/plain", draggingId);
requestAnimationFrame(() => tile.classList.add("is-dragging"));
});
grid.addEventListener("dragover", (e) => {
if (!draggingId) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const over = e.target.closest(".tile");
if (!over || over.dataset.id === draggingId) return;
grid.querySelectorAll(".is-over").forEach((n) => n.classList.remove("is-over"));
over.classList.add("is-over");
// Swap toward the hovered tile, biased by which half of it the cursor is in.
const rect = over.getBoundingClientRect();
const after = e.clientX > rect.left + rect.width / 2;
const overIndex = order.indexOf(over.dataset.id);
const fromIndex = order.indexOf(draggingId);
let target = overIndex;
if (after && fromIndex > overIndex) target = overIndex + 1;
if (!after && fromIndex < overIndex) target = overIndex - 1;
move(draggingId, target);
});
grid.addEventListener("dragleave", (e) => {
const over = e.target.closest && e.target.closest(".tile");
if (over) over.classList.remove("is-over");
});
grid.addEventListener("drop", (e) => e.preventDefault());
grid.addEventListener("dragend", () => {
grid.querySelectorAll(".is-over, .is-dragging").forEach((n) =>
n.classList.remove("is-over", "is-dragging")
);
draggingId = null;
});
/* ---------- keyboard fallback ---------- */
let liftedId = null;
function setLift(id) {
grid.querySelectorAll(".tile").forEach((t) => {
const on = t.dataset.id === id;
t.classList.toggle("is-lifted", on);
t.setAttribute("aria-selected", String(on));
});
liftedId = id;
}
grid.addEventListener("keydown", (e) => {
const tile = e.target.closest(".tile");
if (!tile) return;
const id = tile.dataset.id;
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
if (liftedId === id) {
setLift(null);
status.textContent = "Dropped " + id + ". Order: " + order.join(", ");
} else {
orderBeforeLift = [...order];
setLift(id);
status.textContent = "Picked up " + id + ". Use arrow keys to move.";
}
return;
}
if (e.key === "Escape" && liftedId) {
e.preventDefault();
order = orderBeforeLift;
render();
setLift(null);
tile.focus();
status.textContent = "Cancelled. Order: " + order.join(", ");
return;
}
if (liftedId !== id) return;
const step = { ArrowLeft: -1, ArrowUp: -1, ArrowRight: 1, ArrowDown: 1 }[e.key];
if (step === undefined) return;
e.preventDefault();
move(id, order.indexOf(id) + step);
tile.focus();
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Draggable Bento Rearrange</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="demo" data-demo="Draggable Bento Rearrange">
<header class="demo__head">
<h1>Dashboard tiles</h1>
<p id="hint">
Drag a tile to reorder it. Keyboard: focus a tile, press <kbd>Space</kbd> to pick up,
<kbd>←</kbd>/<kbd>→</kbd> to move, <kbd>Space</kbd> to drop, <kbd>Esc</kbd> to cancel.
</p>
</header>
<ul id="grid" class="bento" role="listbox" aria-label="Dashboard tiles" aria-describedby="hint">
<li class="tile tile--wide" draggable="true" tabindex="0" role="option" aria-selected="false" data-id="revenue">
<span class="tile__kicker">Revenue</span>
<span class="tile__value">$48.2k</span>
<span class="tile__delta tile__delta--up">+12.4%</span>
</li>
<li class="tile" draggable="true" tabindex="0" role="option" aria-selected="false" data-id="users">
<span class="tile__kicker">Active users</span>
<span class="tile__value">9,412</span>
<span class="tile__delta tile__delta--up">+3.1%</span>
</li>
<li class="tile" draggable="true" tabindex="0" role="option" aria-selected="false" data-id="churn">
<span class="tile__kicker">Churn</span>
<span class="tile__value">1.8%</span>
<span class="tile__delta tile__delta--down">-0.4%</span>
</li>
<li class="tile tile--tall" draggable="true" tabindex="0" role="option" aria-selected="false" data-id="traffic">
<span class="tile__kicker">Traffic</span>
<span class="tile__value">184k</span>
<span class="tile__spark" aria-hidden="true"></span>
</li>
<li class="tile" draggable="true" tabindex="0" role="option" aria-selected="false" data-id="tickets">
<span class="tile__kicker">Open tickets</span>
<span class="tile__value">27</span>
<span class="tile__delta tile__delta--down">-6</span>
</li>
<li class="tile tile--wide" draggable="true" tabindex="0" role="option" aria-selected="false" data-id="uptime">
<span class="tile__kicker">Uptime (30d)</span>
<span class="tile__value">99.98%</span>
<span class="tile__delta tile__delta--up">SLA met</span>
</li>
</ul>
<p class="demo__status" role="status" aria-live="polite" id="status"></p>
</main>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
export function BentoDragRearrange() {
const [items, setItems] = useState(["Alpha", "Beta", "Gamma"]);
return (
<section className="demo">
<h2>Draggable Bento Rearrange</h2>
<p>Vanilla behavior maps cleanly to Pointer Events and DOM state.</p>
<div className="stack">
{items.map((item, index) => (
<button
className="item"
key={item}
onClick={() => setItems([...items.slice(0, index), ...items.slice(index + 1), item])}
>
{item}
</button>
))}
</div>
</section>
);
}Draggable Bento Rearrange
Rearrange dashboard tiles with native drag events while preserving a responsive CSS Grid.
Support notes
CSS Grid and columns do most of the layout work. The drag demo adds native DnD only for rearrangement; keep tile order in application state when integrating it.
Included demo
- Vanilla HTML, CSS, and JavaScript with zero external dependencies.
- React equivalent using the same interaction model.
- A Lab route for trying the behavior in isolation.
Integration checklist
Keep state updates separate from presentation, preserve semantic labels, and add persistence or server callbacks at the boundary where your product needs them.