Skip to main content
A drag holds state in the browser for as long as the pointer is down, and reporting every pointer move to the server would make it feel slow. This is the case HTMX does not cover. An island handles it instead: a small piece of JavaScript mounted over the board, which moves the card immediately and persists the new position behind it.

What you’ll have

  • Columns and cards with stable positions
  • Cards you can drag within a column and between columns
  • Visual feedback while dragging, and a marker showing where the card will land
  • A card that moves instantly and rolls back if the server refuses

Which parts of Feather this uses

Islands

The draggable config and this.optimistic(), rather than hand-written pointer events.

OrderingMixin

A position column, and __ordering_scope__ to make it per-column.

API routes

The first route in the series that returns JSON rather than HTML.

feather check

Two rules here catch an island nothing mounts, or a mount with no island.

Step 1: Scaffold

Nothing to do. You need the SQLite database, the Column and Card models, the HTMX routes and the two partials, all of which part 2 produced.

Step 2: Run the prompt

The prompt

Step 3: Test it

  1. Grab a card by its handle and drag it. The card should dim while it moves.
  2. Drop it above or below another card in the same column.
  3. Drag one into a different column.
  4. Watch it land immediately rather than after a pause.
  5. Refresh. The positions held.
Then test the failure path, which is the part most implementations get wrong:
The card should snap back where it came from rather than sitting in a position the database doesn’t know about.

What the framework did

Ordering comes from a mixin. OrderingMixin brought position, move_to(), insert_at_end() and reorder_all(). The service calls them; nothing recalculates indexes by hand. See Models. __ordering_scope__ is the line that matters. Without it, positions would be unique across the whole cards table and two columns would fight over them. With it, each column’s cards start again at zero. The island didn’t call fetch. this.api is the CSRF-aware helper, and raw fetch is a feather check error, because a POST without the token fails in production and works in nobody’s test. See Islands. Rollback is handled for you. this.optimistic() takes the instant change and the server call, and rolls the first back if the second fails. That’s the rollback you tested above. The route returned JSON. Every other route so far returned HTML because the browser was going to insert it. Here the island already moved the element and only needs to report where it landed, so routes/api/ is the right home. See Routes.

Next: add real users

Google sign-in, roles, an admin panel, file attachments and PDF export.