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.

contracts/inputs & outputscore/gesture & motionrocket/host lifecycleexamples/server adapters

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.

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.

PD rockets license ↓

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.

LIVE / KANBAN
drag or use Alt + arrows

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 }

Kanban Rocket source ↗ · JSX template ↗

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.

LIVE / SORTABLE
drag onto an item

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
First item
Second item
Third item
<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 }

Sortable Rocket source ↗ · JSX template ↗

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.

LIVE / DRAG GROUP
drag between lists

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

Sketch a card
Review the draft

Later

Share the update
<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 }

Drag group Rocket source ↗ · JSX template ↗

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.

LIVE / NESTED HOSTS

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

Move this whole item
Inner A
Inner B
Another outer item

Second region

Destination item

Host ownership source ↗ · Pointer lifecycle ↗

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.

LIVE / BENTO
drag between grids or resize ↘

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

Traffic
Tasks
Notes

Scratchpad

Schedule
Ideas
Drafts
<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 }] }

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.

LIVE / FILE TREE
drag between directories

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
src
components
button.ts
panel.ts
index.ts
docs
README.md
<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 }

Tree Rocket source ↗ · JSX template ↗

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.

◈   PD / SIGNAL STATION   ·   LIVE EXPERIMENT 008

Make some waves.

Same Kanban Rocket. A different universe. Drag a transmission or move it with the keyboard.

01 / DISCOVER

Uncharted

02
MAP / 01
AUDIO / 02

END OF CHANNEL

02 / IN MOTION

In orbit

02
FIELD / 03
POST / 04

END OF CHANNEL

03 / COMPLETE

Transmitted

01
BEACON / 05

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 ↗.

MINI EXPERIMENT / 002

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
THE BACKLOG / 05 LEFT
A skeleton for one wordALMOST READY
A loader that never resolvesSTILL LOADING
Duplicated logic that driftsOUT OF SYNC
Megabytes of JavaScriptBUNDLE: HUGE
Rebuilding the browser in JSDIY PLATFORM
Elsa said it best...Drop the baggage.Release to remove it from the model.

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

SurfaceYour markup & layoutRocket styling hooks
Kanban[data-kanban-lane] and [data-kanban-lane-cards] set lane geometry.[data-drop-active], [data-drop-before], [data-drop-end]
Sortable listStyle 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.

STEP 09 / CONTEXTUAL ACTIONS

A menu at the point of intent

Right-click a row or use its Actions button. The server renders one inert template; Rocket clones it on demand, binds the row’s contextId, handles focus and nested menus, and emits a semantic action. The page decides what that action means and returns a small SSE patch. No menu fetch is needed for these shared, non-sensitive actions.

<article data-context-id="record-a" data-menu-for="record-menu">
  <button data-menu-for="record-menu" aria-haspopup="menu">Actions</button>
</article>
<rocket-context-menu id="record-menu" data-on:rocket-menu-action="$menu = evt.detail; @post('/menu-action')">
  <template data-rocket-menu>
    <button role="menuitem" data-action="inspect">Inspect {contextId}</button>
    <button role="menuitem" data-submenu="more" aria-haspopup="menu">More →</button>
    <div id="more" role="menu" popover="auto">
      <button role="menuitem" data-action="archive">Archive</button>
    </div>
  </template>
</rocket-context-menu>

rocket-menu-action → { action, contextId }

Menus may nest as deeply as your markup needs. Opening focuses the menu; Down starts at the first item (Up starts at the last). Arrow keys or h/j/k/l move through a level, Right opens a submenu, and Left, Backspace or Escape returns one level. Escape at the root closes the menu; Enter or Space on the menu activates its first item. Home/End and per-host data-key-* overrides also work. On macOS,Ctrl+n / Ctrl+p also move next / previous, following familiar Control-key text navigation; Command-key browser shortcuts remain untouched. On other platforms the Control pair can be opted into with data-key-focus-next="ArrowDown j Ctrl+n" and data-key-focus-previous="ArrowUp k Ctrl+p". Native popovers provide the top layer and light dismiss; CSS anchors position the menu and flip nested panels at viewport edges, with measured coordinates as a fallback. Your CSS owns the presentation. Context placeholders bind in text, data-menu-param-*, aria-label, and title attributes; executable directives are left as server-rendered.

For fresh options, fetch page-owned HTML before opening and morph the template, or render a direct child marked data-rocket-menu-content for live server markup. Then call menu.openFor(trigger). Live markup stays in the host after close; use menu.closeMenu(refocus?) and menu.isOpen() when coordinating a page-owned menu lifecycle. rocket-menu-scope emits { root, active } when its keyboard scope opens or closes. Additional non-sensitive values can be passed as menu.openFor(trigger, undefined, { label: "Example" }) and used as {label} in the template. The Rocket never fetches menus or makes authorization decisions. See menu behavior ↗ and JSX adapter ↗.

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.

FIELD NOTE / EXAMPLEA small observation

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.

AttributeDefaultPurpose
data-key-select-next↓ / jFocus next card
data-key-select-previous↑ / kFocus previous card
data-key-select-left← / hFocus card in previous lane
data-key-select-right→ / lFocus card in next lane
data-key-move-upAlt + ↑ / kMove above previous card
data-key-move-downAlt + ↓ / jMove below next card
data-key-move-leftAlt + ← / hMove to previous lane
data-key-move-rightAlt + → / lMove to next lane
data-key-cancelEscClear 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

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} />

View the JSX adapter source ↗

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>

View the Go server source ↗

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 ↗