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///releases/latest/download/pd-rockets-browser.tar.gz" | tar -xz -C public/js
# serve the upstream datastar-rocket.js at /js/datastar-rocket.js
# choose one of the following:
`}
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.
-
{browserBundles.map(({ file }) => (
-
{file}↓ {" "} ({(bundleSizes.get(file)! / 1000).toFixed(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.
{`
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.
{`
First item
Second item
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.
{`
Sketch a card
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.
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.
{`
Traffic
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 }] }`}
Bento Rocket source ↗ ·{" "} Placement rule ↗ ·{" "} JSX template ↗
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.
{`
src
…files…
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 …complete example…`}
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.
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 + →.
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.
{`
Queue
First task
Next task
`}
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-height
expose 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:
{`
Moving: First task
Place above
Place at end
`}
{`.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.
{`
Title from server
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 and
grid-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 with
html/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')",
},
};
`}
Go template
{`
{{range .Columns}}
{{range .Cards}}
{{.Title}}
{{end}}
{{end}}
`}
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 ↗