Rocket components · drag & drop first
Drag-and-drop for
server-rendered pages.
PD rockets is a collection of vendorable Rocket components. Drag-and-drop is the first family: your backend renders the items, a custom element handles the gesture, and your application decides what the emitted event means.
FIELD GUIDE / 01
How it works
The public contracts live in contracts/. Core owns pointer capture, the detached preview, target marking and post-move FLIP. The Rocket hosts own DOM lookup and event emission. Neither layer knows which Datastar action your page will invoke or how your backend stores changes.
Why Rocket?
Rocket is Datastar’s web component API. Here it owns element setup and cleanup around browser-only work—pointer capture, hit testing, previews and animation—while the backend stays responsible for rendering state. Semantic custom events let any page connect those mechanics to its own Datastar actions. Rocket gives each instance a lifecycle and a public DOM boundary without turning signals into a second application model.
STEP 01 / GET STARTED
Install
Download the prebuilt release archive, then serve one surface bundle or the full kit alongside the open-source Datastar + Rocket runtime. Each surface includes its own core dependencies; the core bundle is also available for custom mechanics. Render the tags and data attributes from any backend.
mkdir -p public/js
curl -fsSL "https://github.com/<owner>/<repo>/releases/latest/download/pd-rockets-browser.tar.gz" | tar -xz -C public/js
# serve the upstream datastar-rocket.js at /js/datastar-rocket.js
<script type="importmap">{"imports":{"pd-rockets/rocket":"/js/datastar-rocket.js"}}</script>
# choose one of the following:
<script type="module" src="/js/rocket-sortable-tree.js"></script>
<script type="module" src="/js/rocket-kit.js"></script>Get the Rocket runtime ↗ · Upstream MIT notice ↗
The import map resolves pd-rockets/rocket to the pinned upstream module. If your page supplies a separate Rocket ES module, map that specifier to its URL instead; it must export rocket and use the same Datastar instance as the page. The guide uses the latest pinned upstream Datastar + Rocket bundle (v1.0.4) with its MIT notice.
rocket-core.js↓ (7.0 kB br) .br ↓rocket-kanban.js↓ (4.3 kB br) .br ↓rocket-sortable-list.js↓ (3.8 kB br) .br ↓rocket-drag-group.js↓ (4.1 kB br) .br ↓rocket-bento-workspace.js↓ (5.9 kB br) .br ↓rocket-sortable-tree.js↓ (4.6 kB br) .br ↓rocket-context-menu.js↓ (3.2 kB br) .br ↓rocket-inline-edit.js↓ (0.6 kB br) .br ↓rocket-kit.js↓ (16.1 kB br) .br ↓
Sizes are Brotli-compressed kilobytes (1 kB = 1,000 bytes). Use the regular .js file in script tags; the optional .br file is for servers configured to serve precompressed JavaScript with Content-Encoding: br.
STEP 02 / LIVE EXAMPLE
Kanban board
Each lane carries a numeric data-col; cards carry stable IDs. Drag a card into another lane, or focus a card and press Alt+→. The emitted event describes the target lane and the card to insert before; it does not perform a mutation. Plain arrows navigate focus; use Alt+h/j/k/l to stage keyboard moves, then release Alt to commit.
Kanban shortcuts
Focus an item first.
- ↑ ↓ ← → / h j k l
- Focus cards within and between lanes
- Alt + ↑ ↓ ← → / h j k l
- Stage a card move
- Release Alt
- Commit the move
- Esc
- Cancel staging
Backlog
Doing
Done
<rocket-kanban-board>
<section data-kanban-lane data-col="0">
<div data-kanban-lane-cards>
<article data-kanban-card="card-a" tabindex="0">
<button data-kanban-card-main>Card title</button>
</article>
</div>
</section>
</rocket-kanban-board>
rocket-kanban-move → { cardId, col, before }STEP 03 / LIVE EXAMPLE
Sortable list
A sortable list uses the shared pointer lifecycle but chooses its own target geometry and semantic event. Drag above or below an item to insert at that position. Up/down arrows or j/k navigate focused items; Home/End jump to the first or last item. Alt + up/down stages a reorder; release Alt to commit or press Escape to cancel.
Sortable list shortcuts
Focus an item first.
- ↑ ↓ / j k
- Focus previous or next item
- Home / End
- Focus first or last item
- Alt + ↑ ↓ / j k
- Stage a reorder
- Release Alt / Esc
- Commit / cancel the move
<rocket-sortable-list>
<div data-sortable-item="list-a" tabindex="0">First item</div>
<div data-sortable-item="list-b" tabindex="0">Second item</div>
</rocket-sortable-list>
rocket-sortable-move → { itemId, before }STEP 04 / LIVE EXAMPLE
Move between lists
A drag group coordinates several lists without assigning Kanban columns or card semantics. Move an item within a list or into another list, including the space after its last item. Each group is its own drag scope; the page decides how to apply the emitted move. Plain arrows move focus within and between lists. Focus an item and use Alt + arrows (or h/j/k/l), then release Alt to commit. Escape cancels the staged move.
Drag group shortcuts
Focus an item first.
- ↑ ↓ / k j
- Focus within a list
- ← → / h l
- Focus a neighboring list
- Home / End
- Focus first or last item in a list
- Alt + ↑ ↓ ← → / h j k l
- Stage a move or change lists
- Release Alt / Esc
- Commit / cancel the move
Inbox
Later
<rocket-drag-group>
<section data-drop-list="inbox">
<div data-drag-item="note-a" tabindex="0">Sketch a card</div>
</section>
<section data-drop-list="later"></section>
</rocket-drag-group>
rocket-drag-group-move → { itemId, fromList, toList, before }COMPOSITION / LIVE EXAMPLE
Nested Rockets
A sortable list sits inside an item of a drag group. Drag or use Alt + arrows on an inner item to reorder only that list; drag the outer item to move the whole group item. Each host emits its own event, and the page patches the matching example over SSE. Semantic events still bubble; when nesting two hosts of the same surface, the page should check the event target before invoking an outer action.
Nested hosts shortcuts
Focus an item first.
- Tab
- Focus an outer item or an inner list item
- Outer: ↑ ↓ ← → / h j k l
- Navigate items and regions
- Outer: Alt + ↑ ↓ ← → / h j k l
- Move the whole outer item
- Inner: ↑ ↓ / j k
- Navigate inside the sortable list
- Inner: Alt + ↑ ↓ / j k
- Reorder only the inner list
- Release Alt / Esc
- Commit / cancel the move
First region
Second region
STEP 05 / LIVE EXAMPLE
Bento grids
Two CSS grids share one drag scope. Drop a tile on a cell in either grid, or use its ↘ handle to resize it. Displaced tiles preview their new cells while you drag or resize. Rocket sends every changed position on commit; the backend applies them and returns HTML. Plain arrows navigate tiles within and across grids with arrows or h/j/k/l. Focus a tile: Alt + arrows move it by a cell and cross a board boundary at an edge; Alt + Page Up/Down switches grids directly, and Shift + arrows resize. Release the modifier to commit; Escape cancels.
The browser proposes positions for its live preview. The synthetic backend checks the complete resulting grid for bounds and overlap before accepting them; a consuming backend validates its own layout rules.
Bento grids shortcuts
Focus an item first.
- ↑ ↓ ← → / h j k l
- Focus tiles, including across grids
- Home / End
- Focus first or last tile
- Alt + ↑ ↓ ← → / h j k l
- Stage a tile move; cross at an edge
- Alt + Page Up / Down
- Move to the previous or next grid
- Shift + ↑ ↓ ← →
- Stage a resize
- Release modifier / Esc
- Commit / cancel the move
Overview
Scratchpad
<rocket-bento-workspace>
<div data-bento-grid="overview" data-columns="4">
<article data-bento-item="tile-a" data-bento-col="1" data-bento-row="1"
data-bento-width="2" data-bento-height="2" tabindex="0">
Traffic <button data-bento-resize aria-label="Resize Traffic">↘</button>
</article>
</div>
<div data-bento-grid="scratchpad" data-columns="4"></div>
</rocket-bento-workspace>
rocket-bento-move → { itemId, fromGrid, toGrid, updates: [{ itemId, grid, col, row, width, height }] }
rocket-bento-resize → { itemId, grid, updates: [{ itemId, grid, col, row, width, height }] }STEP 06 / LIVE EXAMPLE
File tree
Reorder files and folders, or drop onto a folder to move an entry inside it—even when it is empty. Nested entries move with their folder. Plain up/down arrows (or j/k) navigate visible rows; right expands or enters a folder, and left collapses it or returns to its parent. Focus a row: Alt + up/down reorders among siblings, Alt + right moves it into the preceding folder, and Alt + left moves it out. At the first or last child, Alt + up/down also moves it before or after the parent folder. Release Alt to commit; Escape cancels.
File tree shortcuts
Focus an item first.
- ↑ ↓ / k j
- Focus visible rows
- → / l, ← / h
- Expand or enter / collapse or leave a folder
- Home / End
- Focus first or last visible row
- Alt + ↑ ↓ / k j
- Reorder; cross out at a folder boundary
- Alt + → / l
- Move into the preceding folder
- Alt + ← / h
- Move out after the parent folder
- Release Alt / Esc
- Commit / cancel the move
<rocket-sortable-tree>
<div data-tree-children data-tree-parent="">
<div data-tree-node="src" data-tree-kind="folder">
<div data-tree-row tabindex="0">src</div>
<div data-tree-children data-tree-parent="src">…files…</div>
</div>
</div>
</rocket-sortable-tree>
rocket-tree-move → { itemId, fromParent, toParent, before }STEP 07 / SERVER HANDOFF
Server round trip
Bind each semantic event to a Datastar action in your page. Your server handler validates the target, updates authoritative state, and sends complete HTML over SSE. On this page, a site-only fetch shim stands in for that handler and returns a datastar-patch-elements event. Datastar performs the morph; Rocket animates items from their prior positions to the new ones. Try a demo gesture: the small activity queue shows the Rocket event, the Datastar POST and the SSE patch returned by the fixture, without recording item text or request content.
event: datastar-patch-elements
data: selector #kanban-demo
data: mode outer
data: elements <div id="kanban-demo">…complete example…</div>Browser fixture source ↗ · Go SSE handler ↗
The page seeds only interaction-detail signals. Board, list and grid content live in rendered DOM, not signals.
STEP 08 / YOUR DESIGN SYSTEM
Make it yours
Pick the surface bundle you need and render its light-DOM contract with your own components, classes, and content. PD rockets supplies interaction behavior, not a required stylesheet. Keep the host tag, stable item IDs, focusable items, and the data-* hooks; style everything around them to fit your product.
If your HTML morph keys elements by DOM id, give each card a stable, board-scoped ID too. That keeps an in-flight animation attached to the same card when another card leaves its lane.
Your server-rendered markup is the design surface. Use your own component classes and CSS custom properties for colors, spacing, and shape; Rocket’s data-* attributes expose the interaction states. There is no mandatory theme or token set.
Make some waves.
Same Kanban Rocket. A different universe. Drag a transmission or move it with the keyboard.
Uncharted
END OF CHANNEL
In orbit
END OF CHANNEL
Transmitted
END OF CHANNEL
Move a card between channels: the demo’s page-owned handler updates its model and morphs confirmed markup over SSE. The moving hologram and destination label are rendered from the two template outlets below. The animated backdrop is a decorative canvas; cards, focus, and drag targets remain HTML. See the canvas source ↗ and theme CSS ↗.
Delete the clichés.
Some SPA tropes deserve the bin. Drag one across, or focus it and use Alt + →.
Trope disposal shortcuts
Focus an item first.
- ↑ ↓ / j k
- Focus a trope
- Alt + → / l
- Stage a move into the bin
- Release Alt / Esc
- Dispose / cancel
This is a rocket-drag-group with a playful destination. The page handler interprets a move to bin as deletion, then returns the remaining HTML over SSE. The poof is decoration; the model change is confirmed by the morph. A short canvas particle burst celebrates the bin without adding anything to the Rocket core. Particle source ↗
Set shortcuts on the host
This sortable list keeps arrow keys and replaces Vim j/k with n/p. It stages reorders with Alt + those same keys. Render the attributes with the host; bindings are resolved when the component connects. The event is an intent: your page applies it and patches the confirmed HTML from its backend.
<section class="project-queue" aria-labelledby="queue-title">
<h2 id="queue-title">Queue</h2>
<rocket-sortable-list
data-key-focus-next="ArrowDown n"
data-key-focus-previous="ArrowUp p"
data-key-move-down="Alt+ArrowDown Alt+n"
data-key-move-up="Alt+ArrowUp Alt+p"
data-on:rocket-sortable-move="
$itemId = evt.detail?.['itemId'] ?? null;
$before = evt.detail?.['before'] ?? null;
@post('/queue/move')">
<article class="queue-item" data-sortable-item="item-a" tabindex="0">First task</article>
<article class="queue-item" data-sortable-item="item-b" tabindex="0">Next task</article>
</rocket-sortable-list>
</section>The rocket-sortable-move detail is { itemId, before }; an empty before appends. The example route and signals belong to the page, not the bundle. Set a shortcut attribute to an empty string to disable that intent; see the keyboard reference for the other surfaces and Kanban’s legacy aliases.
Style the states, not the internals
Scope styles under your component class. The host and items are ordinary light-DOM elements; preview and target attributes are styling hooks. By default the floating preview is a clone attached to the document body, so an item class lets it keep your theme outside the host. Give pointer items touch-action: none and a visible keyboard focus state:
.project-queue, .queue-item[data-drag-preview] {
--queue-accent: var(--color-accent, #256c62);
--queue-surface: var(--color-surface, #fff);
}
.project-queue rocket-sortable-list {
display: grid;
gap: .5rem;
position: relative;
}
:is(.project-queue [data-sortable-item], .queue-item[data-drag-preview]) {
position: relative;
padding: .75rem 1rem;
border: 1px solid var(--queue-accent);
border-radius: var(--radius-card, .5rem);
background: var(--queue-surface);
cursor: grab;
touch-action: none;
}
.project-queue [data-sortable-item]:focus-visible {
outline: 2px solid var(--queue-accent);
outline-offset: 2px;
}
.project-queue [data-dragging] { opacity: .45; }
.queue-item[data-drag-preview] { box-shadow: 0 12px 24px #0003; }
.project-queue [data-drop-before]::before,
.project-queue rocket-sortable-list[data-drop-end]::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 3px;
background: var(--queue-accent);
pointer-events: none;
}
.project-queue [data-drop-before]::before { top: -5px; }
.project-queue rocket-sortable-list[data-drop-end]::after { bottom: -5px; }Replace the preview or target markup
For richer affordances, render inert <template> fragments with your items and host. A direct-child data-rocket-preview template on an item replaces that item’s floating clone; its class is copied onto the preview wrapper, which moves under document.body. Its size is yours to style; --rocket-source-width and --rocket-source-heightexpose the original dimensions if useful. Direct-child data-rocket-target templates on the host supply target decorations. Rocket inserts their content into a noninteractive [data-rocket-target-indicator] wrapper at the active target for both pointer and keyboard staging. Give target items and containers position: relative so you can position the indicator inside them:
<!-- Inside a server-rendered [data-sortable-item] -->
<template data-rocket-preview class="queue-preview">
<strong>Moving: First task</strong>
</template>
<!-- Direct children of the rocket-sortable-list host -->
<template data-rocket-target="before">
<span class="queue-target">Place above</span>
</template>
<template data-rocket-target="end">
<span class="queue-target">Place at end</span>
</template>.queue-preview[data-drag-preview] {
display: grid;
place-items: center;
width: max-content;
min-height: var(--rocket-source-height);
border: 2px solid var(--color-accent, #256c62);
border-radius: var(--radius-card, .5rem);
background: var(--color-surface, #fff);
}
.project-queue [data-rocket-target-indicator] {
left: 0;
right: 0;
color: var(--queue-accent);
}
.project-queue [data-rocket-target-indicator="before"] { top: -1.5rem; }
.project-queue [data-rocket-target-indicator="end"] { bottom: -1.5rem; }
.project-queue .queue-target { display: block; }Without a preview template Rocket clones the source item. Without a target template the existing data-drop-* states remain available for CSS-only markers like those above. When using a template indicator, replace those pseudo-element marker rules with your indicator styles. Other target kinds are into for tree folders and cell for bento grids; a bare data-rocket-target template can serve every kind on a host. Geometry and the semantic move event still belong to the surface.
Match the affordance to the layout
| Surface | Your markup & layout | Rocket styling hooks |
|---|---|---|
| Kanban | [data-kanban-lane] and [data-kanban-lane-cards] set lane geometry. | [data-drop-active], [data-drop-before], [data-drop-end] |
| Sortable list | Style the host and [data-sortable-item] rows. | [data-drop-before] on an item; [data-drop-end] on the host |
| Drag group | [data-drop-list] regions contain [data-drag-item]. | [data-drop-active], [data-drop-before], [data-drop-end] |
| Bento | [data-bento-grid] provides tracks and rows; tile positions come from your model. | [data-bento-target], [data-bento-projecting], [data-bento-resizing] |
| File tree | [data-tree-children] nests rows; honor [hidden] on collapsed folders. | [data-tree-before], [data-tree-into], [data-tree-end] |
All surfaces expose [data-dragging] on the source, [data-drag-preview] on the detached clone, and [data-key-staging] on the host during keyboard moves. Bento also needs data-columns to match its CSS grid tracks, a fixed grid-auto-rows, and tile grid-column/grid-row styles that match their rendered position data. See the example CSS ↗ for complete layout and state rules.
INTERACTION / INLINE EDIT
Edit a title in place
Double-click the title, change it, then press Enter or leave the field. Escape cancels. The Rocket recognizes the two title presses even when a parent captures the pointer; it emits request, commit and cancel events. The page owns edit mode, input state and saving.
Double-click the title to try the page-owned local demo.
<rocket-inline-edit data-context-id="card-a">
<span data-inline-edit-trigger data-inline-edit-value>Title from server</span>
<input data-inline-edit-input aria-label="Edit title" value="Title from server">
</rocket-inline-edit>
rocket-inline-edit-request → { contextId }
rocket-inline-edit-commit → { contextId, value }
rocket-inline-edit-cancel → { contextId }The element leaves classes and layout to your CSS; it never submits a request or stores a second copy of the title. See editor behavior ↗ and JSX adapter ↗.
QUICK REFERENCE / 02
Reference
These are browser-facing contracts. Action bindings, permissions and transport configuration belong to the consuming application.
All surfaces support unmodified arrow-key focus navigation and macOS Ctrl+n / Ctrl+p for next / previous focus; other platforms can opt in per host. List, group, grid and tree surfaces also support Home/End. Alt + arrows stage moves, where supported, without changing focus until the morph.
Keyboard attributes
Every host accepts space-separated data-key-<intent> bindings; an empty attribute disables that intent. Focus intents are focus-next, focus-previous,focus-left, focus-right, focus-first and focus-last; movement uses move-up/down/left/right and cancel. Each surface uses only its applicable directions. Bento also accepts resize-up/down/left/right andgrid-previous/next. The shared defaults are in core/keyboard.ts.
Kanban also accepts the original data-key-select-* aliases below; a corresponding data-key-focus-* takes precedence.
| Attribute | Default | Purpose |
|---|---|---|
data-key-select-next | ↓ / j | Focus next card |
data-key-select-previous | ↑ / k | Focus previous card |
data-key-select-left | ← / h | Focus card in previous lane |
data-key-select-right | → / l | Focus card in next lane |
data-key-move-up | Alt + ↑ / k | Move above previous card |
data-key-move-down | Alt + ↓ / j | Move below next card |
data-key-move-left | Alt + ← / h | Move to previous lane |
data-key-move-right | Alt + → / l | Move to next lane |
data-key-cancel | Esc | Clear target marks |
Override a slot with space-separated key tokens, e.g. data-key-select-next="ArrowDown j". Kanban compatibility defaults come from contracts/kanban.ts.
DOM and events
| Host | Descendants | Emitted event |
|---|---|---|
rocket-kanban-board | [data-kanban-lane][data-col], [data-kanban-card] | rocket-kanban-move { cardId, col, before }rocket-kanban-select { cardId } |
rocket-sortable-list | [data-sortable-item] | rocket-sortable-move { itemId, before } |
rocket-drag-group | [data-drop-list] + [data-drag-item] | rocket-drag-group-move { itemId, fromList, toList, before } |
rocket-bento-workspace | [data-bento-grid] + [data-bento-item] | rocket-bento-move / rocket-bento-resize |
rocket-sortable-tree | [data-tree-node] + [data-tree-children] | rocket-tree-move { itemId, fromParent, toParent, before } |
Events bubble and cross the custom-element boundary. before: "" means append. Keep IDs stable across renders so morph and FLIP can match items.
SERVER EXAMPLES / 04
Contractual obligations
The Hono JSX adapter packages the required markup and event binding into a component; this guide uses it to send moves to its in-browser fixture. The Go example writes the same DOM withhtml/template and handles moves on the server. Your page chooses the Datastar action that receives each event.
Hono JSX
const move = {
event: "rocket-kanban-move",
attrs: {
"data-on:rocket-kanban-move":
"$cardId = evt.detail?.['cardId'] ?? null; " +
"$col = evt.detail?.['col'] ?? null; " +
"$before = evt.detail?.['before'] ?? null; @post('/move')",
},
};
<KanbanBoard id="kanban-board" columns={columns} move={move} />Go template
<rocket-kanban-board id="kanban-board"
data-on:rocket-kanban-move="$cardId = evt.detail?.['cardId'] ?? null;
$col = evt.detail?.['col'] ?? null;
$before = evt.detail?.['before'] ?? null; @post('/move')">
{{range .Columns}}
<section data-kanban-lane="" data-col="{{.ID}}">
<div data-kanban-lane-cards="">
{{range .Cards}}
<article data-kanban-card="{{.ID}}" tabindex="0">{{.Title}}</article>
{{end}}
</div>
</section>
{{end}}
</rocket-kanban-board>TAKE IT FURTHER / 05
Run locally
The static docs use a browser-only fixture. The Hono JSX and Go examples show two server renderers for the same contract.
# Static guide + in-browser SSE fixture
bun run serve:site
# Hono JSX demo
bun run demo
# Go demo (after bun run build:client)
cd examples/go && go run .Site server source ↗ · Hono server source ↗ · Go server source ↗