First things, first... What the hack does HSX stand for? I'll say it's 'HTML for Server-Side eXtensions'. :0
But, honestly, I prefer: HTMX Slaps Xtremely :)
SSR-only JSX/TSX renderer for Deno that hides HTMX-style attributes away during
the rendering process, and compiles them to hx-* attributes.
Disclaimer: this was a quick hack in my free time, held together by vibe coding and espresso. I like it a lot, but consider it an early release. I feel it is getting better (a lot)
- SSR-only rendering - Outputs HTML; the optional client bus handles SSE and application events when a page uses them.
- HTMX as HTML - Write
get,post,target,swapas native attributes - Type-safe routes - Branded
Route<Path>types with automatic parameter inference - Co-located components -
hsxComponent()bundles route + handler + render - Page guardrails -
hsxPage()enforces semantic, style-free layouts - Branded IDs -
id("name")returnsId<"name">typed as"#name" - Auto HTMX injection -
<script src="/static/htmx.js">injected when needed - No manual hx-* - Throws at render time if you write
hx-getdirectly - Widgets - Define once, serve via SSR or embed as iframes with Declarative Shadow DOM
- Generative UI - AI models select and render widgets via tool calling, streamed to the browser via SSE + HTMX
- Agent-operable - Any
hsxComponentthat declaresdescribe+inputbecomes an AI tool; an agent drives your real endpoints from the same definition that serves humans - HSX Lens - Opt-in dev workbench that maps page samples, components, widgets, targets, and agent tools into a hypermedia manifest
deno add jsr:@srdjan/hsxOr import directly:
import { id, render, route } from "jsr:@srdjan/hsx";HSX is a monorepo with seven packages:
// Core - JSX rendering, type-safe routes, hsxComponent, hsxPage, SSE
import {
Fragment,
hsxComponent,
hsxPage,
id,
render,
renderSSE,
route,
} from "jsr:@srdjan/hsx";
// Styles - ready-to-use CSS with theming support
import { HSX_STYLES_PATH, hsxStyles } from "jsr:@srdjan/hsx-styles";
// Widgets - embeddable widget protocol + SSR/embed adapters + GenUI catalog
import { widgetToHsxComponent } from "jsr:@srdjan/hsx-widgets/ssr";
import { createCatalog, type GenUIWidget } from "jsr:@srdjan/hsx-widgets";
// GenUI - AI-powered generative UI with tool calling
import {
createGenUIHandler,
createGenUIRoutes,
createInMemoryConversationRepository,
} from "jsr:@srdjan/hsx-genui";
import { claudeProvider } from "jsr:@srdjan/hsx-genui/claude";
// Agent - make your app agent-operable (components become AI tools)
import { createAppAgent } from "jsr:@srdjan/hsx-agent";
// MCP - serve those components to external MCP clients (Claude Code, ...)
import { createMcpHandler } from "jsr:@srdjan/hsx-mcp";
// Lens - inspect the app's hypermedia contract during development
import { createHsxLens } from "jsr:@srdjan/hsx-lens";Install individually:
deno add jsr:@srdjan/hsx
deno add jsr:@srdjan/hsx-styles
deno add jsr:@srdjan/hsx-widgets
deno add jsr:@srdjan/hsx-genui
deno add jsr:@srdjan/hsx-agent
deno add jsr:@srdjan/hsx-mcp
deno add jsr:@srdjan/hsx-lensFor smaller bundles, import only what you need:
// Core only - render, route, id, Fragment (smaller bundle)
import { Fragment, id, render, route } from "jsr:@srdjan/hsx/core";
// Components only - hsxComponent, hsxPage
import { hsxComponent, hsxPage } from "jsr:@srdjan/hsx/components";
// Everything (default)
import { hsxComponent, hsxPage, render, route } from "jsr:@srdjan/hsx";Clone and import using workspace package names:
import { hsxComponent, hsxPage, id, render, route } from "@srdjan/hsx";Note: This shows the low-level API using
route(). For most projects, use the hsxComponent pattern below instead.
import { id, render, route } from "@srdjan/hsx";
const routes = {
todos: route("/todos"),
};
const ids = {
list: id("todo-list"),
};
function Page() {
return (
<html>
<head>
<title>HSX Demo</title>
</head>
<body>
<form post={routes.todos.bind()} target={ids.list} swap="outerHTML">
<input name="text" required />
<button type="submit">Add</button>
</form>
<ul id="todo-list">{/* items */}</ul>
</body>
</html>
);
}
Deno.serve(() => render(<Page />));Output HTML:
<html>
<head>
<title>HSX Demo</title>
</head>
<body>
<form hx-post="/todos" hx-target="#todo-list" hx-swap="outerHTML">
<input name="text" required>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
<script src="/static/htmx.js"></script>
</body>
</html>import { hsxComponent } from "jsr:@srdjan/hsx";
export const TodoList = hsxComponent("/todos", {
methods: ["GET", "POST"],
async handler(req) {
if (req.method === "POST") {
const form = await req.formData();
await addTodo(String(form.get("text")));
}
return { todos: await getTodos() }; // must match render props
},
render({ todos }) {
return (
<ul id="todo-list">
{todos.map((t) => <li key={t.id}>{t.text}</li>)}
</ul>
);
},
});
// Use as route in JSX
<form post={TodoList.bind()} target="#todo-list" swap="outerHTML" />;
// Use as handler in your server
if (TodoList.match(url.pathname)) return TodoList.handle(req);TypeScript enforces that handler returns the same shape that render expects.
methods defaults to ["GET"]. Component handlers return the rendered tree
with an HTML content type, whether that tree is a fragment or a full document.
Page.Component returns a PageRenderable. Rendering <Page.Component />
through render() or renderHtml() therefore applies the same page validation
and script-injection policy as Page.render().
Choose one style: Use either
hsxComponent(recommended) or the low-levelroute()API, but don't mix them in the same project.
hsxPage() wraps a render function that returns a complete HTML document
and validates that:
- The root is
<html>with<head>then<body> - Semantic tags (header/main/section/article/h1-h6/p/ul/ol/li/etc.) have no
classor inlinestyle <style>tags live in<head>; CSS belongs there, not inline- Composition stays within semantic HTML + HSX components
import { hsxComponent, hsxPage } from "jsr:@srdjan/hsx";
const Widget = hsxComponent("/data", {
handler: () => ({ message: "Hi" }),
render: ({ message }) => <p>{message}</p>,
});
const Page = hsxPage(() => (
<html lang="en">
<head>
<title>Guarded Page</title>
<style>{"body { font-family: system-ui; }"}</style>
</head>
<body>
<header>
<h1>Welcome</h1>
</header>
<main>
<section>
<div data-surface="card">
<Widget.Component />
</div>
</section>
</main>
</body>
</html>
));
Deno.serve(() => Page.render());| HSX Attribute | Renders To | Description |
|---|---|---|
get |
hx-get |
HTTP GET request |
post |
hx-post |
HTTP POST request |
put |
hx-put |
HTTP PUT request |
patch |
hx-patch |
HTTP PATCH request |
delete |
hx-delete |
HTTP DELETE request |
query |
hx-query |
HTTP QUERY request |
target |
hx-target |
Element to update |
swap |
hx-swap |
How to swap content |
trigger |
hx-trigger |
Event that triggers request |
vals |
hx-vals |
Additional values (JSON) |
headers |
hx-headers |
Custom headers (JSON) |
behavior="boost" |
hx-boost="true" |
Enable boost mode (<a> only) |
include |
hx-include |
Extra values to send |
statusSwap |
hx-status:CODE |
Per-status swap overrides |
targetInherited |
hx-target:inherited |
Pass a target to descendants |
HTMX 4 defaults implicitInheritance to false, so a parent's value only
reaches its descendants through an Inherited alias. The full table, including
select, indicator, sync, confirm, preserve, encoding, validate,
config, and the morph and history attributes, is in
docs/HTMX_INTEGRATION.md.
Supported elements: form, button, a, div, span, section,
article, ul, tbody, tr
Use route() to create type-safe routes with automatic parameter extraction:
const routes = {
users: {
list: route("/users"),
detail: route("/users/:id"),
posts: route("/users/:userId/posts/:postId"),
},
};
// Bind before JSX. Path parameters are type-checked and URL-encoded:
<button get={routes.users.detail.bind({ id: 42 })}>View</button>;
// Renders: <button hx-get="/users/42">View</button>Use id() to create type-safe element references:
const ids = {
list: id("todo-list"), // Type: Id<"todo-list"> = "#todo-list"
count: id("item-count"),
};
// In JSX:
<ul id="todo-list">...</ul>
<button get="/todos" target={ids.list} swap="innerHTML">Refresh</button>
// Renders: <button hx-get="/todos" hx-target="#todo-list" hx-swap="innerHTML">Create reusable wrapper components that pass through HSX attributes for cleaner JSX:
import type { HsxSwap, Urlish } from "jsr:@srdjan/hsx";
function Card(props: {
children: unknown;
title?: string;
get?: Urlish;
trigger?: string;
swap?: HsxSwap;
}) {
return (
<div
data-surface="card"
data-layout="stack"
data-gap="4"
get={props.get}
trigger={props.trigger}
swap={props.swap}
>
{props.title && <h2>{props.title}</h2>}
{props.children}
</div>
);
}
function Subtitle(props: { children: string }) {
return (
<div data-ui="prose">
<p>{props.children}</p>
</div>
);
}Usage:
function Page() {
return (
<main>
<h1>Dashboard</h1>
<Subtitle>Content loads lazily</Subtitle>
<Card
get={routes.stats.bind()}
trigger="load"
swap="innerHTML"
title="Statistics"
>
<LoadingSkeleton />
</Card>
<Card title="Team Members">
<UserList />
</Card>
</main>
);
}This pattern keeps your page components clean while maintaining full access to
HSX attributes. See the examples/*/components.tsx files for more examples.
Add this to your deno.json:
JSX intrinsic element types (<div>, <form>, <button>, etc.) are
automatically included via the jsx-runtime import, with no additional type
configuration needed.
HSX injects <script src="/static/htmx.js"> when HTMX is used. You must serve
it:
if (url.pathname === "/static/htmx.js") {
const js = await Deno.readTextFile("./vendor/htmx/htmx.js");
return new Response(js, {
headers: { "content-type": "text/javascript; charset=utf-8" },
});
}HSX includes an optional Auras-based CSS module with a single bundled stylesheet:
import { HSX_STYLES_PATH, hsxStyles } from "jsr:@srdjan/hsx-styles";
// Serve the styles
if (url.pathname === HSX_STYLES_PATH) {
return new Response(hsxStyles, {
headers: { "content-type": "text/css; charset=utf-8" },
});
}
// In your page head
<link rel="stylesheet" href={HSX_STYLES_PATH} />;Exports:
hsxStyles- Auras Elements + HSX brand layerHSX_STYLES_PATH- Default path:/static/hsx.css
The package imports both CSS files as text modules. Importing
@srdjan/hsx-styles does not require Deno filesystem read permission.
What you get:
- Layout via data attributes:
data-layout="row|col|stack|cluster|grid|container",data-gap,data-align,data-justify,data-grid-min - Surfaces via attributes:
data-surface="card"anddata-surface="notice" - Button variants via
data-variant="solid|soft|ghost" - Theme controls via
data-theme="dark",data-contrast="more", anddata-motion="reduce"
Customization: Override CSS variables in your page:
<style>
{`:root { --primary: #10b981; --bg: #f0fdf4; --border: #a7f3d0; }`}
</style>;Core tokens include --primary, --primary-hover, --primary-subtle, --bg,
--surface, --surface-raised, --border, --text, --text-muted, spacing
(--space-*), radius (--radius-*), typography (--font-display, --text-*,
--leading-*), shadows (--shadow-*), and layout tokens such as
--container-max.
To force dark mode for a specific page, add data-theme="dark" to <html>.
The @srdjan/hsx-widgets package provides a widget protocol for building
components that work in two contexts: SSR through HSX routes, and embeddable
iframe shells for third-party pages.
A widget is a typed record with validation, styles, rendering, and optional data loading:
import type { Widget } from "jsr:@srdjan/hsx-widgets";
import { fail, ok } from "jsr:@srdjan/hsx-widgets";
export const greetingWidget: Widget<GreetingProps> = {
tag: "hsx-greeting",
props: { validate(raw) {/* ... */} },
styles: `.hsx-greeting { padding: 1rem; }`,
render: (props) => (
<div class="hsx-greeting">
<h2>{props.name}</h2>
</div>
),
load: async (params) => ok({ name: params.name, message: `Hello!` }),
shadow: "open", // Optional: Declarative Shadow DOM
};Use widgetToHsxComponent() to bridge a widget into an HSX route:
import { widgetToHsxComponent } from "jsr:@srdjan/hsx-widgets/ssr";
const GreetingRoute = widgetToHsxComponent(greetingWidget, {
path: "/widgets/greeting/:name",
});
if (GreetingRoute.match(url.pathname)) return GreetingRoute.handle(req);Serve iframe shells with createEmbedHandler(), then embed with a snippet:
<div data-hsx-uri="https://yoursite.com/embed/hsx-greeting?name=World"></div>
<script src="https://yoursite.com/static/hsx/snippet.js"></script>See docs/WIDGETS.md for the full widget guide including Declarative Shadow DOM, style hoisting, and the build pipeline.
The @srdjan/hsx-genui package lets AI models render interactive widgets
directly in a chat interface. The AI selects from a catalog of pre-registered
widgets via tool calling, and the HSX client bus streams the rendered HTML to
the browser over SSE.
A GenUI widget extends the regular Widget protocol with AI metadata - a description, JSON Schema for props, and optional few-shot examples:
import type { GenUIWidget } from "jsr:@srdjan/hsx-widgets";
import { ok } from "jsr:@srdjan/hsx-widgets";
const weatherWidget: GenUIWidget<WeatherProps> = {
tag: "hsx-weather",
description:
"Shows current weather for a city with temperature and conditions",
schema: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
},
required: ["city"],
},
category: "display",
props: {
validate(raw) {
/* ... */ return ok({ city: raw.city });
},
},
styles: `.weather { padding: 1rem; }`,
render: ({ city, temp }) => (
<div class="weather">
<h3>{city}</h3>
<p>{temp}</p>
</div>
),
load: async (params) => {
const data = await fetchWeather(params.city);
return ok(data);
},
};Register widgets in a catalog, then wire up the GenUI handler with an AI provider:
import { createCatalog } from "jsr:@srdjan/hsx-widgets";
import {
createGenUIHandler,
createGenUIRoutes,
createInMemoryConversationRepository,
} from "jsr:@srdjan/hsx-genui";
import { claudeProvider } from "jsr:@srdjan/hsx-genui/claude";
// 1. Register widgets
const catalog = createCatalog([weatherWidget, chartWidget]);
// 2. Create handler with Claude provider
const handler = createGenUIHandler({
catalog,
provider: claudeProvider({ model: "claude-sonnet-4-6" }),
});
// 3. Create routes (page shell + POST endpoint + SSE stream)
const repository = createInMemoryConversationRepository();
const { page, send, stream } = createGenUIRoutes({ handler, repository });
// 4. Serve
Deno.serve((req) => {
const { pathname } = new URL(req.url);
if (page.match(pathname)) return page.handle(req);
if (send.match(pathname)) return send.handle(req);
if (stream.match(pathname)) return stream.handle(req);
return new Response("Not Found", { status: 404 });
});The user types a message, the AI selects a registered widget tool, HSX renders it server-side, and the result streams into the page via SSE. Catalogs expose only registered widgets. They do not include a raw-content tool.
Include AI-readable design constraints in the system prompt:
import { createDesignGuidelines, formatForAI } from "jsr:@srdjan/hsx-widgets";
const guidelines = createDesignGuidelines({
colors: "Use indigo accent. All colors via CSS custom properties.",
});
const handler = createGenUIHandler({
catalog,
provider,
systemPrompt: formatForAI(guidelines),
includeGuidelines: false, // we provided our own
});The core @srdjan/hsx package also exports renderSSE() for building custom
SSE endpoints from any async iterable of JSX:
import { renderSSE } from "jsr:@srdjan/hsx/core";
async function* generateWidgets() {
yield <div>Loading...</div>;
const data = await fetchData();
yield <Chart data={data} />;
}
Deno.serve(() => renderSSE(generateWidgets()));HTMX 4 removed SSE from core, so the HSX client bus drives it. The bus opens the
EventSource and inserts each frame at the position named by swap:
<div sseConnect="/stream" sseSwap="message" swap="beforeend">
{/* widgets appear here as SSE events arrive */}
</div>;Serve the bus at /static/hsx-bus.js next to HTMX. renderSSE ends every
stream with an hsx-sse-close frame, which the bus needs: a browser
EventSource reconnects whenever the server closes the stream.
For hand-written streams, encodeSSEFrame({ event, data, id?, retry? }) accepts
one frame object. It rejects invalid event names, IDs, and retry values.
The @srdjan/hsx-agent package turns your existing application into an agent
action space. Where GenUI lets an AI render display widgets, this lets an AI
drive your real endpoints: each hsxComponent that declares describe +
input becomes an AI tool from the same definition that serves humans over
HTMX. The agent calls the component's own handle() and reads back the exact
semantic HTML a person would see. One definition, two consumers.
A component is agent-callable only when it declares both describe and
input. Components without them are invisible to the agent - no surprise
mutations.
import { createAppAgent, toRequest } from "@srdjan/hsx-agent";
import type { AgentToolInvoker } from "@srdjan/hsx-agent";
import { claudeProvider } from "@srdjan/hsx-genui/claude";
// The same component the human form posts to is now also an AI tool:
const AddTodo = hsxComponent("/todos", {
methods: ["POST"],
describe: "Add a new todo item to the list.",
input: {
schema: {
type: "object",
properties: { text: { type: "string", description: "The todo text." } },
required: ["text"],
},
},
async handler(req) {
const form = await req.formData();
addTodo(String(form.get("text")));
return {};
},
render: () => <TodoListView />,
});
// Humans use it via HTMX (unchanged):
<form post={AddTodo.bind()} swap="none" />;
// The agent uses the same definition as a tool:
const invokeTool: AgentToolInvoker = ({ args, component, sourceRequest }) =>
component.handle(toRequest(component, args, sourceRequest));
const agent = createAppAgent({
components: [AddTodo, ToggleTodo, ClearDone],
provider: claudeProvider({ model: "claude-sonnet-4-6" }),
invokeTool,
});The shared agent registry compiles every declared input schema with Ajv's JSON
Schema 2020-12 validator. Schemas without $schema use 2020-12. Invalid schemas
fail when the registry is created, and invalid arguments never reach
invokeTool.
invokeTool is required. The application owns authorization and execution at
this boundary and receives the validated arguments, component, and originating
request. The simple adapter above preserves the component HTTP path; production
applications can enforce tenant, session, or capability policy first.
AppAgent is structurally a GenUI handler ({ handleTurn, tools }), so it
drops straight into the pre-built chat routes:
import {
createGenUIRoutes,
createInMemoryConversationRepository,
} from "@srdjan/hsx-genui";
const repository = createInMemoryConversationRepository();
const { send, stream } = createGenUIRoutes({
handler: agent,
repository,
basePath: "/copilot",
});handleTurn({ turnId, message, history, sourceRequest }) emits rendered frames,
transcript messages, and one terminal event. The repository owns branded
conversation and turn IDs, atomic turn claims, immutable snapshots, and bounded
frame replay. Reconnecting with Last-Event-ID resumes after the acknowledged
frame without running the tool effects again. The send route queues execution,
so a missing stream request cannot block later turns. Transient claim failures
retry three times by default; claimRetry can inject the wait capability or
adjust the bounded attempt count. Exhaustion atomically fails the pending turn
with safe error and close frames instead of blocking the conversation. A cursor
older than the retained replay window receives HTTP 409 instead of an incomplete
suffix; malformed cursors and cursors beyond the stored turn receive HTTP 400.
If a resumable browser connection closes instead of retrying, the bus marks the
stream as failed and renders a generic refresh-to-recover message.
The agent's tool calls invoke the real components; each rendered fragment is
streamed to the browser and fed back to the model as its observation. In the
todos-copilot example the mutating components render the list with swapOob,
so whether a change comes from the human form or the agent, the canonical
#todo-list updates out-of-band while the agent narrates in the chat. Run it
with ANTHROPIC_API_KEY=... deno task example:todos-copilot.
createAgentRegistry(components) and
toRequest(component, args, sourceRequest) are exported for custom integrations
that need the same schema validation and request construction.
The @srdjan/hsx-mcp package mounts a Model Context Protocol endpoint into your
existing Deno.serve, so external MCP clients - Claude Code, Claude Desktop,
any MCP-capable tool - can discover and operate the same agent-callable
components. The in-app copilot needs a chat UI and an API key; the MCP endpoint
needs neither. Write a component once and it serves humans over HTMX, your
embedded copilot over SSE, and every MCP client over JSON-RPC, all through the
component's own handle().
import { toRequest } from "@srdjan/hsx-agent";
import type { AgentToolInvoker } from "@srdjan/hsx-agent";
import { createMcpHandler } from "@srdjan/hsx-mcp";
const invokeTool: AgentToolInvoker = ({ args, component, sourceRequest }) =>
component.handle(toRequest(component, args, sourceRequest));
const mcpToken = Deno.env.get("MCP_TOKEN");
if (!mcpToken) throw new Error("MCP_TOKEN is required");
const mcp = createMcpHandler({
components: todoComponents,
invokeTool,
serverName: "todos",
bearerToken: mcpToken,
});
Deno.serve((req) => {
const mcpResponse = mcp.handle(req);
if (mcpResponse) return mcpResponse;
// Your app routes...
return new Response("Not found", { status: 404 });
});Connect from Claude Code:
claude mcp add --transport http todos http://localhost:8000/mcpTool results carry the HTTP status plus the same rendered HTML a human receives,
so the agent observes the app as a user sees it. Pass a lens manifest
(createHsxManifest() output) as manifest to expose the app's full hypermedia
contract as MCP resource hsx://manifest.
The endpoint is stateless Streamable HTTP: POST-only, plain JSON responses, no
sessions, and no SDK dependency. It accepts protocol versions 2025-11-25,
2025-06-18, and 2025-03-26. A missing MCP-Protocol-Version header defaults
to 2025-03-26; malformed or unsupported values return HTTP 400 before JSON-RPC
dispatch or tool effects. Initialize negotiation still selects the requested
supported version or the newest supported version.
Security: MCP tools mutate real application state. Never mount the endpoint
on a publicly reachable server without authorize or bearerToken.
The @srdjan/hsx-lens package is an opt-in development workbench for the
hypermedia contract your app already declares. It renders explicit page samples,
collects author-time HSX attributes before they become hx-*, mirrors
component/widget/agent metadata, and serves a local manifest plus an HTML
workbench. It does not call component handlers.
import { createHsxLens } from "@srdjan/hsx-lens";
const lens = createHsxLens({
appName: "Todos",
pages: [{ name: "Home", path: "/", render: () => <Page.Component /> }],
components: todoComponents,
});
Deno.serve((req) => {
const lensResponse = lens.handle(req);
if (lensResponse) return lensResponse;
// Your app routes...
return new Response("Not found", { status: 404 });
});By default, the workbench is served at /__hsx and the JSON manifest at
/__hsx/manifest.json. Mount it only in local or trusted development
environments.
Renders JSX to an HTTP Response.
render(<Page />, {
status: 200, // HTTP status code
headers: {}, // Additional response headers
maxDepth: 100, // Max nesting depth (DoS protection)
maxNodes: 50000, // Max node count (DoS protection)
injectHtmx: undefined, // true/false to force, undefined for auto
});Renders JSX to an HTML string.
const html = renderHtml(<Page />, {
maxDepth: 100,
maxNodes: 50000,
injectHtmx: undefined,
});Creates a type-safe route template. bind() requires exactly the parameters
extracted from :param segments and returns the final URL value used in JSX.
const r = route("/users/:id");
// r.path = "/users/:id"
// r.bind({ id: 42 }).path = "/users/42"
// r.match("/users/42") = { id: "42" }Creates a branded element ID with # prefix.
const listId = id("todo-list");
// Type: Id<"todo-list">
// Value: "#todo-list"JSX Fragment for grouping elements without a wrapper.
<Fragment>
<li>One</li>
<li>Two</li>
</Fragment>;Co-locates a route, request handler, and render function.
const Comp = hsxComponent("/items/:id", {
methods: ["GET", "DELETE"], // defaults to ["GET"]
status: 200,
headers: { "x-powered-by": "hsx" },
handler: async (_req, params) => ({
item: await getItem(params.id),
}),
render: ({ item }) => <div>{item.name}</div>,
});
// Bind the component route before using it in JSX:
<button delete={Comp.bind({ id: 42 })} target="#row-42" />;Unexpected handler or render errors return a generic 500 response, while
handlers can throw HsxHttpError for intentional boundary failures such as
invalid input.
Run examples with deno task:
| Example | Command | Description |
|---|---|---|
| Todos | deno task example:todos |
Full CRUD with partial updates |
| Active Search | deno task example:active-search |
Live search as you type |
| Lazy Loading | deno task example:lazy-loading |
Deferred content loading |
| Form Validation | deno task example:form-validation |
Server-side validation |
| Polling | deno task example:polling |
Live dashboard with intervals |
| Auras Showcase | deno task example:auras-showcase |
First-impression demo, live scene playground, compact reference |
| Tabs & Modal | deno task example:tabs-modal |
Tab navigation and modals |
| HSX Components | deno task example:hsx-components |
Co-located route + handler + render |
| HSX Page | deno task example:hsx-page |
Semantic full-page with hsxPage guardrails |
| Low-Level API | deno task example:low-level-api |
Manual render/renderHtml without hsxPage/hsxComponent |
| HSX Widget | deno task example:hsx-widget |
Widget SSR route + iframe embed shell |
| Todos Copilot | deno task example:todos-copilot |
AI copilot driving real hsxComponent endpoints |
| Index of examples | examples/README.md |
Quick guide to pick the right example |
For the HSX widget example, build client assets first:
deno task build:hsx-widgets
deno task example:hsx-widget- HTML escaping - All text content and attributes are escaped (XSS prevention)
- Raw text elements -
<script>and<style>children are NOT escaped. Never pass user input. - No manual hx-* - Throws at render time. Use HSX aliases instead.
- DoS protection - Optional
maxDepthandmaxNodeslimits
packages/
hsx/ # Core package (@srdjan/hsx)
mod.ts # Main entry point
jsx-runtime.ts # Minimal JSX runtime (compiler requirement)
render.ts # SSR renderer with HTMX injection
sse.ts # SSE response helper (renderSSE, encodeSSEFrame)
loading.ts # Loading placeholder component
hsx-normalize.ts # HSX to hx-* attribute mapping
hsx-types.ts # Route, Id, HsxSwap, HsxTrigger types
hsx-component.ts # hsxComponent factory (route + handler + render)
hsx-page.ts # hsxPage guardrail for full-page layouts
htmx-config.ts # HtmxConfig meta tag (noSwap and other HTMX config)
bus.ts # HsxBus config meta tag
core.ts # Tree-shaking entry: render, route, id, Fragment
components.ts # Tree-shaking entry: hsxComponent, hsxPage
runtime/hsx-bus.js # Optional browser runtime: emit/on/act and SSE
hsx-styles/ # Styles package (@srdjan/hsx-styles)
mod.ts # Entry point (bundles CSS strings)
hsx-brand.css # HSX brand layer
vendor/ # Vendored Auras foundation
hsx-widgets/ # HSX widgets package (@srdjan/hsx-widgets)
mod.ts # Main entry point
widget.ts # Widget protocol
widget-wrapper.ts # Shared Light DOM / Shadow DOM wrapping
ssr-adapter.ts # Widget -> hsxComponent bridge
styles.ts # Style collection for hsxPage
result.ts # Result<T,E> type utilities
genui-widget.ts # GenUIWidget<P> type (Widget + AI metadata)
catalog.ts # Widget catalog and tool definition generation
design-guidelines.ts # AI-readable design system constraints
embed/ # Embed helpers (iframe shell + snippet)
build/ # Dual-compile build pipeline (esbuild + Preact)
hsx-genui/ # Generative UI package (@srdjan/hsx-genui)
mod.ts # Main entry point
provider.ts # AIProvider port (Message, ToolCall, StreamEvent)
agent-loop.ts # Shared provider/tool execution loop
handler.ts # GenUI handleTurn adapter
conversation.ts # Durable repository port + in-memory replay adapter
components.tsx # Chat page, send route, and replayable SSE route
providers/
claude.ts # Claude/Anthropic adapter (raw fetch + SSE)
hsx-agent/ # Agent-operable apps package (@srdjan/hsx-agent)
mod.ts # Main entry point
app-agent.ts # createAppAgent (AI drives real components)
registry.ts # Ajv 2020-12 tool registry and argument validation
request-build.ts # toRequest (validated args + source Request -> Request)
types.ts # AgentComponent and AgentToolInvoker boundaries
hsx-mcp/ # MCP server package (@srdjan/hsx-mcp)
mod.ts # Main entry point
mcp-handler.ts # createMcpHandler (Streamable HTTP endpoint)
protocol.ts # MCP method dispatch (initialize/tools/resources)
versions.ts # Supported protocol versions and manifest defaults
jsonrpc.ts # Hand-rolled JSON-RPC 2.0 envelope
types.ts # McpHandlerOptions, McpHandler
hsx-lens/ # Dev workbench package (@srdjan/hsx-lens)
mod.ts # Main entry point
manifest.ts # createHsxManifest (samples -> manifest)
workbench.tsx # createHsxLens (local HTML + JSON routes)
examples/
todos/ # Full todo app example
todos-copilot/ # Todos with an AI copilot driving real endpoints
active-search/ # Search example
lazy-loading/ # Lazy load example
form-validation/ # Form validation example
polling/ # Polling example
tabs-modal/ # Tabs and modal example
hsx-components/ # HSX Component pattern example
hsx-page/ # hsxPage full-page guardrail example
low-level-api/ # Manual render/renderHtml without hsxPage/hsxComponent
hsx-widget/ # HSX widget SSR + embed shell example
auras-showcase/ # Auras design system with a live HSX scene playground
ats/ # Light DOM custom elements styled by host CSS
event-bus/ # Client-side pub/sub: filters, toast, optimistic add
vendor/htmx/
htmx.js # Vendored HTMX 4.0.0
htmx.d.ts # HTMX config and API types
docs/
EXAMPLES.md # Full examples matrix
USER_GUIDE.md # Comprehensive user guide
HSX_OVERVIEW.md # Architecture overview
HTMX_INTEGRATION.md # HTMX integration details
ROUTES_AND_IDS.md # route() and id() reference
WIDGETS.md # HSX widget guide and embed workflow
plans/ # Dated plans and implementation records
solutions/ # Documented fixes and decisions
MIT - see LICENSE
{ "imports": { "hsx/jsx-runtime": "jsr:@srdjan/hsx/jsx-runtime" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "hsx" } }