Sortable list
A list whose items can be dragged, or moved with the keyboard, into a new place — here or in another list sharing its namespace. A drop fires unmagic-sortable:move and posts the move.
With handles
Only the grip drags, and the grip is the keyboard stop. Nothing is posted here, so the event is shown below the list.
Phone screen
Take-home exercise
Pairing session
Meet the team
<div class="w-96 max-w-full">
<%= sortable_list id: "steps", label: "Interview steps", class: "flex flex-col gap-2" do |list| %>
<% [ "Phone screen", "Take-home exercise", "Pairing session", "Meet the team" ].each_with_index do |step, index| %>
<%= list.item(key: "step-#{index}", rank: index + 1, label: step) do %>
<div class="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white px-2 py-1.5 text-sm dark:border-neutral-800 dark:bg-neutral-900">
<%= sortable_handle label: "Move #{step}" %>
<span class="flex-1"><%= step %></span>
<button type="button" class="<%= button_classes(:icon) %>" aria-label="Edit <%= step %>" title="Edit">
<%= Unmagic::Components::Icons.svg(self, :pencil) %>
</button>
</div>
<% end %>
<% end %>
<% end %>
<output id="steps_log" class="mt-3 block min-h-5 font-mono text-xs text-neutral-500">Drop something to see the move.</output>
<script type="module">
document.getElementById("steps").addEventListener("unmagic-sortable:move", (event) => {
const { key, prev, next } = event.detail
document.getElementById("steps_log").textContent = `moved ${key} · prev ${prev || "—"} · next ${next || "—"}`
})
</script>
</div>A wrapping grid
orientation: :grid. The drop slot follows reading order, and every arrow key moves.
Screenshot 1
Screenshot 2
Screenshot 3
Screenshot 4
Screenshot 5
Screenshot 6
Screenshot 7
Screenshot 8
<%# Written out in full, since Tailwind can only generate classes it can read. %>
<% tiles = [
"bg-sky-100 text-sky-800 dark:bg-sky-400/15 dark:text-sky-300",
"bg-emerald-100 text-emerald-800 dark:bg-emerald-400/15 dark:text-emerald-300",
"bg-amber-100 text-amber-800 dark:bg-amber-400/15 dark:text-amber-300",
"bg-rose-100 text-rose-800 dark:bg-rose-400/15 dark:text-rose-300",
"bg-violet-100 text-violet-800 dark:bg-violet-400/15 dark:text-violet-300",
"bg-teal-100 text-teal-800 dark:bg-teal-400/15 dark:text-teal-300",
"bg-orange-100 text-orange-800 dark:bg-orange-400/15 dark:text-orange-300",
"bg-lime-100 text-lime-800 dark:bg-lime-400/15 dark:text-lime-300"
] %>
<%= sortable_list orientation: :grid, label: "Screenshots", class: "grid grid-cols-2 gap-3 sm:grid-cols-4" do |list| %>
<% tiles.each_with_index do |colours, index| %>
<%= list.item(key: "shot-#{index}", rank: index + 1, label: "Screenshot #{index + 1}") do %>
<div class="grid aspect-video cursor-grab place-items-center rounded-lg text-sm font-medium <%= colours %>">
Screenshot <%= index + 1 %>
</div>
<% end %>
<% end %>
<% end %>Two lists, one namespace
Items move between lists that share a namespace. Left and Right move a held item across.
Shortlist
Ada Lovelace
Grace Hopper
Alan Turing
Maybe later
Katherine Johnson
<div class="grid gap-6 sm:grid-cols-2">
<% { "Shortlist" => [ "Ada Lovelace", "Grace Hopper", "Alan Turing" ], "Maybe later" => [ "Katherine Johnson" ] }.each do |title, people| %>
<div class="rounded-xl border border-neutral-200 p-3 dark:border-neutral-800">
<h3 class="mb-2 text-sm font-semibold"><%= title %></h3>
<%= sortable_list namespace: "people", label: title, params: { list: title },
class: "flex min-h-24 flex-col gap-1.5 rounded-lg p-1 sortable-active:bg-neutral-50 sortable-over:bg-neutral-100 dark:sortable-active:bg-neutral-900 dark:sortable-over:bg-neutral-800" do |list| %>
<% people.each do |person| %>
<%= list.item(key: person.parameterize, label: person) do %>
<div class="flex cursor-grab items-center gap-2 rounded-md bg-white p-2 text-sm shadow-xs dark:bg-neutral-900">
<%= avatar person, size: :small, "aria-hidden": true %> <%= person %>
</div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
</div>