OrderingMixin, __ordering_scope__, islands, the built-in draggable
config, this.optimistic(), and an API route returning JSON.
Prerequisites
- Continue from part 2
- Start fresh
Your app needs the SQLite database, the
Column and Card models, the HTMX routes
and the two partials. Nothing else to do.Step 1: Add OrderingMixin
OrderingMixin adds a position column and the methods to shuffle it.
models/column.py
models/card.py
__ordering_scope__ is the line that matters. Without it, positions would be unique
across the whole table, and cards in different columns would fight over them. With it,
each column’s cards start again at zero.
Step 2: The migration
Step 3: Update the services
services/column_service.py
services/card_service.py
move() handles the awkward case. A card crossing between columns has to leave one
ordering scope and join another, so it moves across first, parks at the end, and only then
slots into position.
Step 4: The move API route
routes/api/board.py
Step 5: The island
static/islands/kanban-board.js
Step 6: Add the drag handle
templates/partials/card.html
data-id so the island can identify the card, and the handle itself.
Step 7: Make the column a drop zone
templates/partials/column.html
data-id on .column-cards is what makes it a drop zone. The empty-placeholder
class exists so the “No cards yet” line disappears when a card arrives.
Step 8: Mount the island
templates/pages/board.html
Islands load from the Vite dev server in debug, for hot reload, and through
feather_asset() in production, which resolves the content-hashed build output. That’s
why islands get their own block rather than sitting in scripts.Step 9: The drag CSS
static/css/app.css
.dragging on the element in flight,
.drag-over on the zone under the pointer, and .feather-drop-placeholder on the marker
showing where it will land. The scaffolded app.css has base styles for all three, and
these are the Kanban-specific overrides. Add them inside the existing
@layer components block.
Step 10: Test it
- Grab a card by its six-dot handle and drag it.
- Drop it above or below another card in the same column.
- Drag one into a different column.
- Watch it land immediately rather than after a round trip.
- Refresh. The positions held.
Prompt Claude
feather check has two rules aimed at exactly this part. missing-island catches a
template mounting an island that doesn’t exist, and orphan-island catches an island
nothing mounts. Both are easy to trip while wiring this up.Checkpoint
- Columns and cards have stable positions
- A card can be dragged within a column and between columns
- The dragged card dims, and a placeholder shows where it will land
- The card lands instantly rather than after a round trip
- Positions survive a refresh
feather checkpasses
Files you created or changed
What you learned
OrderingMixinand the methods it adds__ordering_scope__for positions relative to a parent- Declaring and mounting an island
- The built-in
draggableconfig this.optimistic()and rollback on failure- API routes that return JSON rather than HTML
- How islands load differently in debug and production
Next: add real users
Google sign-in, roles, the admin panel, file attachments and PDF export.