Board
A Trello-style board: columns of cards, both reorderable by pointer or keyboard, with a way to add to each. Drops post to one endpoint (unmagic-sortable's), and the page morphs to the saved order.
A working board
Drag cards within and between columns, drag a column by its header, or add a card or a list. It's saved in your session. On a phone, press and hold a card or a column's header to lift it. With the keyboard: Tab to a card or a column's grip, Space to pick it up, arrows to move it, Space to drop, Escape to cancel.
This example talks to a server
bin/dev) or mount it in an app to try it for real.
Roadmap · 8 cards
To do
3 cardsWrite the job ad for the platform role
Book the room for Thursday's panel
Ask Grace for interview feedback
Add a card
Doing
2 cardsShortlist the backend candidates
Draft the offer letter template
Add a card
Review
1 cardCheck the salary bands against the market
Add a card
Done
2 cardsPost the role on the careers page
Set up the take-home repository
Add a card
Add a list
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-3">
<p class="text-sm text-neutral-500">Roadmap · <%= pluralize(@board.columns.sum { |c| @board.cards_in(c).size }, "card") %></p>
<%= button_to "Reset the board", board_path, method: :delete, class: button_classes(size: :small),
form: { data: { turbo_confirm: "Put every card back where it started?" } } %>
</div>
<%= board id: "roadmap", url: board_order_path, column_height: "32rem" do |b| %>
<% @board.columns.each do |column| %>
<% cards = @board.cards_in(column) %>
<% b.column key: column["key"], rank: column["rank"], title: column["title"], params: { column: column["key"] } do |col| %>
<% col.actions do %>
<%= menu do |m| %>
<% m.link "Rename", "#" %>
<% m.link "Archive", "#", tone: :danger %>
<% end %>
<% end %>
<% cards.each do |card| %>
<% col.card key: card["key"], rank: card["rank"], label: card["title"], id: "board_#{card["key"]}" do %>
<p class="m-0 leading-snug"><%= card["title"] %></p>
<% if card["tag"] || card["owner"] %>
<div class="mt-2.5 flex items-center justify-between gap-2">
<%= badge card["tag"] if card["tag"] %>
<%= avatar card["owner"], size: :small, "aria-hidden": true if card["owner"] %>
</div>
<% end %>
<% end %>
<% end %>
<% col.add url: board_cards_path, field: "card[title]", params: { "card[column]" => column["key"] } %>
<% end %>
<% end %>
<% b.add_column url: board_columns_path, field: "column[title]" %>
<% end %>
</div>Cards with handles
A sortable_handle in a card makes only the grip drag it, so the rest of the card stays a link, and on a touch screen the grip drags at once, without the long press.
Inbox
2 cardsWaiting
1 card<%= board id: "handles", sortable_columns: false do |b| %>
<% [ [ "Inbox", [ "Reply to Ana about Friday", "Send Grace the brief" ] ],
[ "Waiting", [ "Tom's corrected address" ] ] ].each do |title, cards| %>
<% b.column title: title, params: { column: title.downcase } do |col| %>
<% cards.each_with_index do |text, index| %>
<% col.card key: "#{title}-#{index}", class: "flex cursor-default items-center gap-2 p-2" do %>
<%= sortable_handle label: "Move #{text}" %>
<%= link_to text, "#", class: "min-w-0 flex-1 truncate font-medium text-neutral-900 underline-offset-2 hover:underline dark:text-neutral-100" %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>