Build a reorderable priority list with Tailwind CSS. Use a max-w-lg card headed by a title, a count of the items and a reset button. Each row is a card with a grip handle on the left, a large monospace rank number, the item title with a one-line note under it, an owner initial in a round badge on the right, and a small "Up next" pill on whichever row sits at the top. Rows lift slightly on hover and the one being dragged goes semi-transparent with a dark ring. Add a hint line explaining that the handle can be dragged or focused and moved with the arrow keys, and a visually hidden live region. Then add JavaScript using pointer events: capture the pointer on the handle, reorder by comparing the pointer against the midpoints of the other rows, renumber the ranks and move the pill as the order changes, support arrow-key moves from a focused handle, keep focus on the handle after a keyboard move, and announce the new position in the live region. Reset restores the original order.
Paste it into Claude, Cursor, v0 or any AI coding tool. Prefer the
finished markup? .
Build a reorderable priority list with Tailwind CSS. Use a max-w-lg card headed by a title, a count of the items and a reset button. Each row is a card with a grip handle on the left, a large monospace rank number, the item title with a one-line note under it, an owner initial in a round badge on the right, and a small "Up next" pill on whichever row sits at the top. Rows lift slightly on hover and the one being dragged goes semi-transparent with a dark ring. Add a hint line explaining that the handle can be dragged or focused and moved with the arrow keys, and a visually hidden live region. Then add JavaScript using pointer events: capture the pointer on the handle, reorder by comparing the pointer against the midpoints of the other rows, renumber the ranks and move the pill as the order changes, support arrow-key moves from a focused handle, keep focus on the handle after a keyboard move, and announce the new position in the live region. Reset restores the original order.
var list = document.getElementById("rankList");
var liveRegion = document.getElementById("rankStatus");
var original = [].slice.call(list.children);
var dragging = null;
function renumber() {
[].slice.call(list.children).forEach(function (item, index) {
item.querySelector("[data-rank]").textContent = index + 1;
item.querySelector("[data-now]").classList.toggle("hidden", index !== 0);
});
}
function announce(item) {
var index = [].slice.call(list.children).indexOf(item);
liveRegion.textContent =
item.querySelector("[data-title]").textContent +
" moved to position " + (index + 1) + " of " + list.children.length;
}
// Insert before the first row whose midpoint is below the pointer. Comparing
// midpoints keeps the order stable while the cursor sits between two rows,
// which hit-testing the element under the pointer does not.
function rowAfter(y) {
var rows = [].slice.call(list.querySelectorAll("[data-item]:not([data-dragging])"));
for (var i = 0; i < rows.length; i++) {
var box = rows[i].getBoundingClientRect();
if (y < box.top + box.height / 2) return rows[i];
}
return null;
}
list.addEventListener("pointerdown", function (e) {
var grip = e.target.closest("[data-grip]");
if (!grip) return;
dragging = grip.closest("[data-item]");
dragging.setAttribute("data-dragging", "");
dragging.classList.add("opacity-60", "ring-2", "ring-neutral-900", "shadow-lg");
// Capture on the handle so a fast drag that outruns the cursor keeps sending
// moves to this element instead of whatever is underneath it.
grip.setPointerCapture(e.pointerId);
e.preventDefault();
});
list.addEventListener("pointermove", function (e) {
if (!dragging) return;
var after = rowAfter(e.clientY);
if (after) list.insertBefore(dragging, after);
else list.appendChild(dragging);
renumber();
});
function drop() {
if (!dragging) return;
dragging.classList.remove("opacity-60", "ring-2", "ring-neutral-900", "shadow-lg");
dragging.removeAttribute("data-dragging");
announce(dragging);
dragging = null;
}
list.addEventListener("pointerup", drop);
list.addEventListener("pointercancel", drop);
list.addEventListener("keydown", function (e) {
var grip = e.target.closest("[data-grip]");
if (!grip || (e.key !== "ArrowUp" && e.key !== "ArrowDown")) return;
var item = grip.closest("[data-item]");
e.preventDefault();
if (e.key === "ArrowUp" && item.previousElementSibling) {
list.insertBefore(item, item.previousElementSibling);
} else if (e.key === "ArrowDown" && item.nextElementSibling) {
list.insertBefore(item.nextElementSibling, item);
} else {
return;
}
renumber();
announce(item);
grip.focus();
});
document.getElementById("rankReset").addEventListener("click", function () {
original.forEach(function (item) { list.appendChild(item); });
renumber();
liveRegion.textContent = "Order reset";
});
renumber();