Sortable List

List supporting reordering.

Playlist order

Use the move buttons to reorder — no dragging required.

  1. Morning warm-up
  2. Focus block
  3. Wind-down

html

<div class="srt srt--card" style="--accent:#F5C2E0;--gap:6px;">
  <h3 class="srt__heading">Playlist order</h3>
  <p class="srt__hint">Use the move buttons to reorder — no dragging required.</p>
  <ol class="srt__list">
    <li class="srt__row">
      <span class="srt__handle" aria-hidden="true">⠿</span>
      <span class="srt__text">Morning warm-up</span>
      <span class="srt__moves">
        <button type="button" class="srt__move" disabled aria-label="Move Morning warm-up up">↑</button>
        <button type="button" class="srt__move" aria-label="Move Morning warm-up down">↓</button>
      </span>
    </li>
    <li class="srt__row">
      <span class="srt__handle" aria-hidden="true">⠿</span>
      <span class="srt__text">Focus block</span>
      <span class="srt__moves">
        <button type="button" class="srt__move" aria-label="Move Focus block up">↑</button>
        <button type="button" class="srt__move" aria-label="Move Focus block down">↓</button>
      </span>
    </li>
    <li class="srt__row">
      <span class="srt__handle" aria-hidden="true">⠿</span>
      <span class="srt__text">Wind-down</span>
      <span class="srt__moves">
        <button type="button" class="srt__move" aria-label="Move Wind-down up">↑</button>
        <button type="button" class="srt__move" disabled aria-label="Move Wind-down down">↓</button>
      </span>
    </li>
  </ol>
</div>
<style>
.srt { font: 14px/1.4 system-ui, sans-serif; color: #241430; background: #FAF7F2; max-width: 320px; width: 320px; padding: 16px; border-radius: 12px; box-sizing: border-box; }
.srt__heading { margin: 0 0 4px; font-size: 15px; font-weight: 700; }
.srt__hint { margin: 0 0 10px; font-size: 12px; color: #241430a0; }
.srt__list { list-style: none; margin: 0; padding: 0; counter-reset: srt; }
.srt__row { display: flex; align-items: center; gap: 8px; padding: 8px 10px; margin-bottom: var(--gap); border-radius: 8px; }
.srt--card .srt__row { background: #fff; box-shadow: 0 1px 4px rgba(36,20,48,.08); }
.srt--numbered .srt__row { counter-increment: srt; }
.srt--numbered .srt__row::before { content: counter(srt) "."; font-weight: 700; color: var(--accent); }
.srt__handle { color: #24143070; cursor: grab; }
.srt__text { flex: 1; }
.srt__moves { display: flex; gap: 2px; }
.srt__move { width: 24px; height: 24px; border-radius: 6px; border: 1px solid #24143020; background: #fff; cursor: pointer; font-size: 12px; line-height: 1; }
.srt__move:hover:not(:disabled) { background: var(--accent); }
.srt__move:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.srt__move:disabled { opacity: .35; cursor: not-allowed; }
</style>

tailwind

<div class="srt-tw srt-tw--card font-sans text-sm text-[#241430] bg-[#FAF7F2] max-w-[320px] w-[320px] p-4 rounded-xl box-border" style="--accent:#F5C2E0">
  <h3 class="m-0 mb-1 text-[15px] font-bold">Playlist order</h3>
  <p class="m-0 mb-2.5 text-xs text-[#241430a0]">Use the move buttons to reorder — no dragging required.</p>
  <ol class="list-none m-0 p-0">
    <li class="srt-tw__row flex items-center gap-2 py-2 px-2.5 rounded-lg" style="margin-bottom:6px">
      <span aria-hidden="true" class="text-[#24143070] cursor-grab">⠿</span>
      <span class="flex-1">Morning warm-up</span>
      <span class="flex gap-0.5">
        <button type="button" disabled aria-label="Move Morning warm-up up" class="srt-tw__move w-6 h-6 rounded-md border border-[#24143020] bg-white cursor-not-allowed text-xs opacity-35">↑</button>
        <button type="button" aria-label="Move Morning warm-up down" class="srt-tw__move w-6 h-6 rounded-md border border-[#24143020] bg-white cursor-pointer text-xs">↓</button>
      </span>
    </li>
    <li class="srt-tw__row flex items-center gap-2 py-2 px-2.5 rounded-lg" style="margin-bottom:6px">
      <span aria-hidden="true" class="text-[#24143070] cursor-grab">⠿</span>
      <span class="flex-1">Focus block</span>
      <span class="flex gap-0.5">
        <button type="button" aria-label="Move Focus block up" class="srt-tw__move w-6 h-6 rounded-md border border-[#24143020] bg-white cursor-pointer text-xs">↑</button>
        <button type="button" aria-label="Move Focus block down" class="srt-tw__move w-6 h-6 rounded-md border border-[#24143020] bg-white cursor-pointer text-xs">↓</button>
      </span>
    </li>
    <li class="srt-tw__row flex items-center gap-2 py-2 px-2.5 rounded-lg">
      <span aria-hidden="true" class="text-[#24143070] cursor-grab">⠿</span>
      <span class="flex-1">Wind-down</span>
      <span class="flex gap-0.5">
        <button type="button" aria-label="Move Wind-down up" class="srt-tw__move w-6 h-6 rounded-md border border-[#24143020] bg-white cursor-pointer text-xs">↑</button>
        <button type="button" disabled aria-label="Move Wind-down down" class="srt-tw__move w-6 h-6 rounded-md border border-[#24143020] bg-white cursor-not-allowed text-xs opacity-35">↓</button>
      </span>
    </li>
  </ol>
</div>
<style>
.srt-tw--card .srt-tw__row { background: #fff; box-shadow: 0 1px 4px rgba(36,20,48,.08); }
.srt-tw--numbered .srt-tw__row { position: relative; padding-left: 24px; }
.srt-tw__move:hover:not(:disabled) { background: #F5C2E0; }
.srt-tw__move:focus-visible { outline: 2px solid #F5C2E0; outline-offset: 1px; }
</style>

react

import { useState } from "react";

export function SortableList({
  heading = "Playlist order",
  accent = "#F5C2E0",
  style = "card",
  showHandle = true,
  gap = 6,
}) {
  const [items, setItems] = useState(["Morning warm-up", "Focus block", "Wind-down"]);
  const move = (i, dir) => {
    const j = i + dir;
    if (j < 0 || j >= items.length) return;
    const next = [...items];
    [next[i], next[j]] = [next[j], next[i]];
    setItems(next);
  };
  const rowStyle = {
    display: "flex", alignItems: "center", gap: 8, padding: "8px 10px", marginBottom: gap, borderRadius: 8,
    background: style === "card" ? "#fff" : "transparent",
    boxShadow: style === "card" ? "0 1px 4px rgba(36,20,48,.08)" : "none",
  };
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", fontSize: 14, color: "#241430", background: "#FAF7F2", maxWidth: 320, width: 320, padding: 16, borderRadius: 12, boxSizing: "border-box" }}>
      <h3 style={{ margin: "0 0 4px", fontSize: 15, fontWeight: 700 }}>{heading}</h3>
      <p style={{ margin: "0 0 10px", fontSize: 12, color: "#241430a0" }}>Use the move buttons to reorder — no dragging required.</p>
      <ol style={{ listStyle: "none", margin: 0, padding: 0 }}>
        {items.map((item, i) => (
          <li key={item} style={rowStyle}>
            {showHandle && <span aria-hidden="true" style={{ color: "#24143070", cursor: "grab" }}>⠿</span>}
            {style === "numbered" && <span style={{ fontWeight: 700, color: accent }}>{i + 1}.</span>}
            <span style={{ flex: 1 }}>{item}</span>
            <span style={{ display: "flex", gap: 2 }}>
              <button type="button" disabled={i === 0} aria-label={`Move ${item} up`} onClick={() => move(i, -1)} style={{ width: 24, height: 24, borderRadius: 6, border: "1px solid #24143020", background: "#fff", cursor: i === 0 ? "not-allowed" : "pointer", opacity: i === 0 ? 0.35 : 1, fontSize: 12 }}>↑</button>
              <button type="button" disabled={i === items.length - 1} aria-label={`Move ${item} down`} onClick={() => move(i, 1)} style={{ width: 24, height: 24, borderRadius: 6, border: "1px solid #24143020", background: "#fff", cursor: i === items.length - 1 ? "not-allowed" : "pointer", opacity: i === items.length - 1 ? 0.35 : 1, fontSize: 12 }}>↓</button>
            </span>
          </li>
        ))}
      </ol>
    </div>
  );
}