streaming-ui-script Svelte Themes

Streaming Ui Script

Streaming UI Script is a compact, declarative language designed for LLMs to generate UI for their responses. Drop one <script> tag and one <streaming-ui-script> tag into your application code whether it's React, Vue, Angular, Svelte, plain HTML, or no framework at all — and you have a streaming, interactive renderer for an LLM's response.

streaming-ui-script

A framework-agnostic web component that renders LLM-generated UI from Streaming UI Script — a compact, declarative language designed for chat assistants. Drop one <script> tag and one <streaming-ui-script> tag into any HTML page — React, Vue, Angular, Svelte, plain HTML, or no framework at all — and you have a streaming, interactive renderer for an LLM's response.

The library bundles everything needed at runtime:

  • A Streaming UI Script parser (line-oriented, streaming-first, error-tolerant) with single-, double-, and backtick-quoted strings.
  • An evaluator with reactive state, queries, mutations, actions, and 20+ built-in functions (@Count, @Filter, @Sort, @Push, @Concat, @Each, @Sum, @Avg, @Min, @Max, @Round, @Floor, @Ceil, @Abs, …) plus array shortcuts (.length, .first, .last, and field pluck like $rows.title).
  • A React-like DOM reconciler that diffs each re-render against the live DOM — text-input value, selection, IME state, scroll positions, <details>.open, and stateful primitives like Tabs are all preserved across renders. Components that need to hold UI state get a helpers.useInstanceState(...) slot keyed by their position in the tree.
  • A rich component library of 100+ components — layout, content, forms (including Slider, NumberInput, DatePicker, FileUpload, Combobox), tables, charts, feedback & media (Avatar, Progress, Tooltip, HoverCard, Popover, Toast, Toasts, Rating, ProgressRing, ChatBubble, …), navigation (Breadcrumb, Pagination, Navbar, NavbarItem), menus (DropdownMenu, MenuItem, MenuSeparator, MenuLabel), hierarchical data (Tree, TreeNode), chat composites, high-level pattern composites (Hero, Cover, PageHeader, MetricGrid, Toolbar, EmptyState, Timeline, KanbanBoard, Testimonial, PricingTable, MediaCard, …) and app-shell composites (AppShell, Sidebar, SplitView) that render a full SaaS layout in one statement.
  • A built-in JavaScript layerScript(...) (lifecycle-managed, useEffect-style) and @Js(body, args?) (one-shot click handlers with per-item arg capture). Always available.
  • A built-in routing layerRoutes(...), Route(path, content), NavLink(label, to), @Navigate("/path"), and reactive $route + params. Hash-based, framework-agnostic, always on.
  • Seven built-in themes (light, dark, neon, pastel, glass, brutalist, skyline) plus full custom-token support via CSS custom properties.
  • A system prompt generator that emits a clean, ordered prompt teaching the LLM exactly which components, builtins, and tools are available. The build emits two flavours: system_prompt.txt (full — every feature) and system_prompt_chat.txt (compact — only the surfaces a chat reply needs).

Everything lives inside a Shadow DOM, so the renderer's styles never leak into your application — and your application's styles never leak into the renderer.


Why?

LLMs are great at writing structured text, and a small DSL lets them describe a full UI in 60–70% fewer tokens than JSON. This project ships that idea as a single web component, so any framework — or no framework at all — can render generative UI without extra wiring.


Quick start

1. Add the script tag

<script type="module" src="https://asfand-dev.github.io/streaming-ui-script/dist/streaming-ui-script.js"></script>

For non-module setups use the IIFE build:

<script src="https://asfand-dev.github.io/streaming-ui-script/dist/streaming-ui-script.iife.js" defer></script>

The CSS is bundled inside the JS and injected into each instance's shadow root, so you do not need a separate stylesheet.

2. Mount the tag

<streaming-ui-script id="reply" theme="light"></streaming-ui-script>

3. Render a response

There are three equivalent ways to set the program text:

<!-- as an attribute -->
<streaming-ui-script response='root = Card([CardHeader("Hi")])'></streaming-ui-script>

<!-- as inner text -->
<streaming-ui-script>
  root = Card([CardHeader("Hi")])
</streaming-ui-script>

<!-- as a property / method -->
<script>
  const el = document.querySelector("streaming-ui-script");
  el.setResponse(`root = Stack([greeting])
greeting = Card([CardHeader("Hello", "Generative UI in plain HTML")])`);
</script>

4. Stream from your LLM

const response = await fetch("/api/chat", {
  method: "POST",
  body: JSON.stringify({ system: systemPrompt, messages }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
el.streaming = true;
el.clear();
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  el.appendChunk(decoder.decode(value, { stream: true }));
}
el.streaming = false;

5. Send the system prompt to your LLM

Either fetch the auto-generated system_prompt.txt from the CDN:

const systemPrompt = await fetch(
  "https://asfand-dev.github.io/streaming-ui-script/dist/system_prompt.txt",
).then((r) => r.text());

…or build a richer prompt programmatically (with custom rules, tool descriptions, examples, etc.):

const prompt = el.getSystemPrompt({
  preamble: "You are an analytics assistant.",
  additionalRules: ["Always end with a FollowUpBlock of 2 prompts."],
  tools: [{ name: "list_orders", description: "Return recent orders.", argsExample: { limit: 10 } }],
});

6. (Optional) Provide tools

el.setTools({
  list_orders: async ({ limit }) => fetch(`/api/orders?limit=${limit}`).then(r => r.json()),
  update_order: async ({ id, status }) => fetch(`/api/orders/${id}`, { method: "PATCH", body: JSON.stringify({ status }) }).then(r => r.json()),
});

7. (Optional) Listen for assistant messages

el.addEventListener("assistant-message", (event) => {
  appendUserMessageToChat(event.detail.message);
});

Public API

All members live on the <streaming-ui-script> element.

Attributes

Attribute Values Description
theme light, dark, or a JSON object literal Switches the theme. JSON objects are merged with the default light token map.
streaming true / unset Hint that text is still being appended. Useful for status indicators in your app.
response Streaming UI Script text Sets the program declaratively. Re-renders whenever the attribute changes.
showerrors true / unset If present and true, displays parse errors in the rendered UI. Defaults to off.

Script(...) / @Js(...) and hash-based routing (Routes, Route, NavLink, @Navigate) are always on. To skip them in the generated prompt, build the chat-flavoured prompt via getSystemPrompt({ mode: "chat" }).

Properties

Property Type Description
response string Equivalent to setResponse.
tools Record<string, Function> Getter returns the currently-registered tools; setter is equivalent to setTools(...).
streaming boolean Reflects the streaming attribute.
showErrors boolean Reflects the showerrors attribute.
route string (read-only) Current path tracked by the router (e.g. "/users/42").

Methods

Method Description
setResponse(text) Replace the program (one-shot rendering). Resets state and queries.
appendChunk(chunk) Append a streaming chunk and re-render.
clear() Reset state, queries, and the rendered output.
setTheme(name | tokens) Apply a built-in theme by name or a partial token map.
setTools(tools) Register tools used by Query() and Mutation().
registerComponents(specs, root?) Extend the built-in library with your own components.
getSystemPrompt(options?) Build a system prompt that matches the current library and tools.
navigate(path) Programmatically navigate when routing is enabled (updates window.location.hash).

Events

Event Detail When it fires
assistant-message { message: string } When @ToAssistant("...") runs (e.g. a follow-up button).
error { errors: ParseError[] } After each render whose source had parse errors.
route-change { path, previousPath, params, pattern } When the current hash path changes.

The error event always fires regardless of showerrors, so host apps can log or report errors even when the in-page banner is suppressed.


Themes

Seven themes are built in. Pick one with theme="..." or pass a custom token map.

Theme Vibe
light Crisp default, indigo accent.
dark Standard dark surface, indigo accent.
neon Cyberpunk-inspired dark mode with magenta/cyan glow, monospace headings, sharp corners.
pastel Soft, friendly, light & rounded. Lavender + mint palette, generous radii, gentle shadows.
glass Modern glassmorphism — vivid gradient backdrop, frosted translucent surfaces, indigo→cyan accent.
brutalist Neo-brutalism — hard 2px black borders, chunky offset shadows, loud primary, zero gradients.
skyline Enterprise cloud-console aesthetic — deep navy primary, cyan accents, calm pale blue bg.

Custom token maps:

el.setTheme({
  colorPrimary: "#16a34a",
  colorPrimaryHover: "#15803d",
  colorBg: "#f0fdf4",
  radiusMd: "14px",
});

You can also style the host element from outside:

streaming-ui-script {
  --rui-color-primary: #16a34a;
  --rui-radius-md: 14px;
}

A full list of tokens lives in docs/themes.html and src/theme/index.ts.


Icons (Font Awesome)

The runtime auto-loads Font Awesome 6.7.2 from the public CDN — once into document.head and once into each instance's shadow root. Host apps do not need to add a stylesheet.

  • Icon strings are Font Awesome names without the fa- prefix: "house", "chart-line", "star", "cart-shopping", "circle-check", "triangle-exclamation", "sack-dollar".
  • Optional variant prefix: "regular:star", "brands:github". The default variant is solid.
  • Use the dedicated Icon(name, variant?, size?) component to render a standalone glyph (sizexs, sm, md, lg, xl).
  • Every component prop named iconNavLink, SidebarItem, Banner, Notification, FeatureItem, Tag, StatCard, ListItem, TimelineItem, DescriptionItem, Tile, EmptyState, … — now expects a Font Awesome name (or a variant:name string).
  • ProgressRing(value, max?, label?, …) renders label as an icon when it resolves to a Font Awesome name (e.g. "circle-check"), and as plain text otherwise — perfect for completion rings.
  • Invisible Unicode glyph modifiers (variation selectors, ZWJ) are stripped silently so "triangle-exclamation\uFE0F" (a legacy emoji leftover) still resolves to the proper icon instead of falling back to inline text.
brandIcon  = Icon("rocket", "solid", "lg")
homeIcon   = Icon("house")
profileTab = NavLink("Profile", "/profile", "ghost", false, "user")
kpis       = MetricGrid([
  StatCard("Revenue", "$48k", "up",   "+12%", "sack-dollar"),
  StatCard("Orders",  "1,284","up",   "+8%",  "cart-shopping"),
  StatCard("Refunds", "12",   "down", "-3",   "rotate-left")
])

The CDN URL and a tiny ensureFontAwesomeLoaded(shadow) helper live in src/icons/index.ts and are re-exported as FONT_AWESOME_CDN_URL. The custom element calls the helper from its connectedCallback (idempotent — safe to mount multiple instances).


Streaming UI Script in 60 seconds

$days = "7"
data = Query("get_metrics", {days: $days}, {events: 0, daily: []})
filter = FormControl("Range", Select("days", [SelectItem("7","7d"), SelectItem("30","30d")], null, null, $days))
kpi = StatCard("Events", "" + data.events, "up")
chart = LineChart(data.daily.day, [Series("Events", data.daily.events)])
root = Stack([CardHeader("Analytics"), filter, kpi, chart])

Highlights:

  • One statement per line: name = Expression.
  • $variables are reactive — passing one to an Input or Select two-way-binds.
  • Strings come in three flavours: "double", 'single', and `backtick` (multi-line, no escaping required — perfect for JS bodies).
  • Comments are stripped silently: // line, # line (shell-style), and /* block */.
  • Query("tool", {args}, {defaults}, refreshSec?) runs immediately and re-runs when its $variable args change.
  • Mutation("tool", {...}) only runs from @Run(name) inside an Action([...]).
  • @Each(arr, "row", template) iterates inline. The loop variable is scoped strictly to template.
  • @Filter, @Sort, @Count, @Sum, @Avg, @Round, @Push, @Concat and more are available.
  • Array shortcuts: $rows.length, $rows.first, $rows.last, plus pluck ($rows.title[title1, title2, …]).
  • Forward references are allowed — list root = Stack([...]) first and let the children stream in beneath it.

Build a todo app declaratively (no JS required for add/delete)

$todos = [{id: 1, text: "Welcome — try editing", done: false}]
$draft = ""

addBtn = Button("Add", Action([
  @Set($todos, @Push($todos, {id: $todos.length + 1, text: $draft, done: false})),
  @Reset($draft)
]), "primary")

row = Card([Stack([
  TextContent(t.text),
  Button("Delete", Action([@Set($todos, @Filter($todos, "id", "!=", t.id))]), "ghost")
])])

list = @Each($todos, "t", row)
root = Stack([Input("draft-input", "What needs doing?", "text", null, $draft), addBtn, list])

The full reference is on the docs site (docs/language.html).


JavaScript interactions

Script(...) and @Js(...) ship with every renderer — no attribute to flip. The LLM can author two surfaces whenever the full prompt is in use:

  • Script("id", body, deps?) — behaviour-only node that runs on mount and re-runs whenever any listed $variable changes. Lifecycle matches useEffect: cleanup before re-run, disposal on unmount, AbortSignal exposed as ctx.signal.
  • @Js(body, args?) — action step you drop inside Action([...]). The optional args object is evaluated at render time and exposed inside the body as ctx.args. This is the canonical way to feed per-row data from an @Each loop into a click handler.
<streaming-ui-script></streaming-ui-script>
list = @Each($todos, "t", row)
row = Card([Stack([
  TextContent(t.text),
  Button("Toggle", Action([
    @Js(`
      const todos = ctx.state.get('todos') || [];
      ctx.state.set('todos', todos.map(x => x.id === ctx.args.id ? Object.assign({}, x, {done: !x.done}) : x));
    `, {id: t.id})
  ]))
])])

body is a regular string. Use double quotes for one-liners (escape inner " as \" and newlines as \n) or backticks for multi-line code (real newlines and unescaped " are fine). The default (full) system prompt teaches the LLM about these features; for chat-style replies where you want to keep the LLM purely declarative, build the prompt via getSystemPrompt({ mode: "chat" }) — the chat flavour omits the JS section.

See the JavaScript interactions guide or the deeper coding-gen-skill.md for a full end-to-end app walkthrough.

Routing

Hash-based routing is built into the runtime. The LLM may emit routes that stay in sync with the URL (#/dashboard, #/users/42). Browser back/forward, bookmarks, and deep links all work — and the host page never reloads.

<streaming-ui-script></streaming-ui-script>
root = Stack([nav, main])

nav = Stack([
  NavLink("Home",     "/",          "ghost", true),
  NavLink("Dashboard","/dashboard", "ghost"),
  NavLink("Users",    "/users",     "ghost")
], "row", "s")

main = Routes([
  Route("/",           homePage),
  Route("/dashboard",  dashboardPage),
  Route("/users/:id",  userPage),
  Route("*",           notFoundPage)
], "/")

homePage      = Card([CardHeader("Welcome")])
dashboardPage = Card([CardHeader("Dashboard")])
userPage      = Card([CardHeader("User " + params.id)])
notFoundPage  = Callout("warning", "Not found", "We couldn't find " + $route + ".")
  • Routes(items, default?) picks the matching Route based on the current hash path. First match wins; default is the fallback when nothing else matches.
  • Route(path, content) supports literal segments ("/about"), parameter segments ("/users/:id"params.id), and trailing wildcards ("/docs/*"params._).
  • NavLink(label, to, variant?, exact?, icon?) is a router-aware anchor that intercepts clicks and reflects data-active="true" for the current path.
  • @Navigate("/path") is an action step you can drop into any Action([...]) for programmatic navigation. From JS, call el.navigate("/path").
  • The current path is exposed reactively as $route. Inside a matched route's content, the captured params land in the params loop variable.

The default (full) system prompt teaches the LLM about routing. To skip the routing section (e.g. for chat-style replies where you don't want the model inventing URLs), build the prompt via getSystemPrompt({ mode: "chat" }).

See the routing guide and the live routing demo for a full end-to-end walkthrough.

Built-in components

Group Components
Layout Stack, Grid, Section, Container, Spacer, Card, CardHeader, CardBody, CardFooter, Divider, Separator, Tabs, TabItem, Accordion, AccordionItem, Modal, Sheet, Steps, StepsItem, AspectRatio, ScrollArea
Content TextContent, Header, Image, Icon, Link, Badge, Tag, TagBlock, Alert, Callout, Note, Quote, CodeBlock, Skeleton, Markdown, Kbd
Forms Form, FormControl, Input, TextArea, Select, SelectItem, Checkbox, CheckBoxGroup, CheckBoxItem, Radio, Switch, Toggle, ToggleGroup, Button, Buttons, SearchBar, Slider, NumberInput, DatePicker, FileUpload, Combobox
Data Table, Col, List, ListItem, StatCard, Stats, Tile, Progress, ProgressRing, Pagination, Tree, TreeNode
Charts BarChart, LineChart, PieChart, Series
Feedback & Media Avatar, AvatarGroup, PersonChip, Tooltip, HoverCard, Popover, Rating, Toast, Toasts
Navigation Breadcrumb, BreadcrumbItem, Navbar, NavbarItem
Menus DropdownMenu, MenuItem, MenuSeparator, MenuLabel
Chat SectionBlock, ListBlock, FollowUpBlock, FollowUpItem, ActionLink, ChatBubble
Patterns Hero, Cover, PageHeader, SectionHeader, MetricGrid, Toolbar, EmptyState, Timeline, TimelineItem, FeatureGrid, FeatureItem, MediaCard, Testimonial, ProfileCard, Comment, Banner, Notification, KanbanBoard, KanbanColumn, KanbanCard, DescriptionList, DescriptionItem, StatusDot, PricingTable, PricingCard
App shell AppShell, Sidebar, SidebarSection, SidebarItem, SplitView
Scripting Script (always available; chat prompt mode omits it)
Routing Routes, Route, NavLink (always available; chat prompt mode omits them)

Rich pattern composites

The Patterns group is the secret sauce for getting beautiful, dense UI out of an LLM with minimal tokens. Each composite packs an entire idiom (hero, page header, KPI strip, empty state, timeline, kanban, …) into one positional call. Reach for these before composing the equivalent layout from Card + Stack:

root = Stack([dashHeader, kpis, board, follow])
dashHeader = PageHeader("Engineering Q3", "12 active · 4 at risk", ["Workspace", "Engineering"], dashActions, Badge("On track", "success"))
dashActions = [Button("Export", Action([@Run(export_q3)]), "secondary"), Button("New project", Action([@Run(new_project)]), "primary")]
kpis = MetricGrid([StatCard("Active", "12", "flat"), StatCard("At risk", "4", "up", "+2"), StatCard("Shipped", "8", "up", "+3"), StatCard("On-time", "87%", "down", "-3%")])
board = KanbanBoard([
  KanbanColumn("To do",      [KanbanCard("Migrate auth", "Roll out new SDK.", ["auth"], "Asha")]),
  KanbanColumn("Doing",      [KanbanCard("Streaming UI v2", "20 new components.", ["frontend"], "Alex", "primary")]),
  KanbanColumn("Review",     [KanbanCard("Mobile onboarding", "Awaiting design.", ["mobile"], "Wren", "warning")]),
  KanbanColumn("Done",       [KanbanCard("Activity timeline", "Shipped to 100%.", ["shipped"], "Mira", "success")])
])
follow = FollowUpBlock(["Show at-risk projects", "Compare to Q2", "Who needs help?"])

The generated system prompt teaches the LLM about every component, and the ## Design principles + ## Composition recipes sections steer the model toward dashboard / landing / detail-page layouts that look like a shadcn site out of the box.

Add your own with registerComponents:

const ProductCard = {
  name: "ProductCard",
  description: "Product tile with title and price.",
  props: [
    { name: "title", type: "string" },
    { name: "price", type: "number" },
  ],
  render: (_node, props) => {
    const div = document.createElement("div");
    div.textContent = `${props.title} — $${props.price}`;
    return div;
  },
};

el.registerComponents([ProductCard]);

The next call to getSystemPrompt() automatically includes the new component.


LLM integration helper

If you're driving the renderer from an agentic LLM (Cursor, Claude Code, etc.) one companion document is kept in sync with the bundle:

  • coding-gen-skill.md — an extensive knowledge base for building complete applications in Streaming UI Script: mental model, every component group, state management, queries/mutations, actions, loops, JavaScript interactions, routing, app patterns (todo list, dashboard, wizard, chat, settings, real-time, status page, checkout flow, file manager, calendar, docs portal), and anti-patterns. Treat it as the "deep dive" the model can read once and then author full apps unaided. It also opens with a short "When to reach for this library" section so agents can decide quickly whether <streaming-ui-script> is the right tool for the job.

Documentation site

The docs/ folder is the source for the live documentation site published at https://asfand-dev.github.io/streaming-ui-script/. Every page is a static HTML file that loads the same bundle the rest of the world consumes from the CDN.

Page What's on it
index.html Overview, drop-in install, live theme picker.
get-started.html Step-by-step integration walkthrough.
frameworks.html Integration recipes for React, Next.js, Vue, Angular, Svelte, plain HTML.
language.html Full Streaming UI Script language reference.
components.html Every built-in component with a live preview, positional signatures, prop tables, and enum values.
javascript-interactions.html Script(...) + @Js(...) guide — always available at runtime.
routing.html Hash-based routing guide — always available at runtime.
themes.html Built-in themes gallery, live picker, side-by-side compare, and the token customization studio (formerly theme-customization.html, now redirected).
examples.html Curated showcase of real-world block UIs (auth, products, FAQ, cart, todos, …).
playground.html CodeMirror 6 editor with custom Streaming UI Script highlighting + autocomplete, live preview, share links, persistent layout modes (drag the splitter, collapse the docs sidebar), hover-over component info, signature/argument tooltips with allowed enum values, and an inspection mode that maps rendered DOM back to the source. Powered by src/language/.
live-examples.html Catalog page linking out to every standalone demo below.

Live examples

Every standalone demo is a single HTML page that you can open directly. They double as integration recipes — view source on any of them to see how a real host page wires setResponse, appendChunk, setTools, and setTheme.

Demo page Highlights
chat-bot.html OpenAI-powered chat that streams replies into the renderer. Toggle between the chat-flavoured and full system prompt live.
tools-example.html Read / write / poll patterns wired to in-page setTools() handlers.
external-data-example.html Live GitHub repository explorer powered by a single tool function.
support-agent.html AI triage workspace: pick a ticket, the agent suggests priority + draft reply.
analytics-assistant.html Natural-language → charts/KPIs/breakdown.
javascript-todo-app.html Reactive todo list with filters + localStorage persistence via one Script(...).
javascript-pomodoro.html Pomodoro timer with phases, audio chime, and notifications.
javascript-stopwatch.html Sub-second stopwatch + laps using requestAnimationFrame and proper teardown.
routing-demo.html A four-page app driven by Routes + NavLink + @Navigate, deep links, browser back/forward.
app-workspace.html Full SaaS workspace: sticky Sidebar, topbar, MetricGrid, timeline, DescriptionList.
project-dashboard.html Engineering program dashboard: banner, PageHeader, KPIs, kanban board, activity timeline.
marketing-landing.html Hero, feature grid, pricing tiers, testimonials, team line-up, FAQ.
team-directory.html Profile cards, avatar groups, search-with-pagination, empty state, slide-in Sheet.
settings-app.html Tabs, Switch, ToggleGroup, Progress, Kbd, danger-zone confirmation Sheet.
ecommerce-product.html Product page with Cover hero, MediaCard related items, Rating, ProgressRing, Stats, reviews.
inbox-app.html Focused mail/chat inbox using SplitView, SearchBar, PersonChip, Notification, ChatBubble.
pricing-page.html Pricing surface: Cover, Stats trust strip, PricingTable, FeatureGrid, Quote, FAQ, CTA.
crm-contacts.html Directory: SearchBar, segmented filters, Tile quick stats, paginated cards, detail Sheet.
status-page.html Public SRE status: incident Banner, latency LineChart, services with StatusDot, incident Timeline.
checkout-flow.html Four-step checkout wizard: Steps, SplitView cart, promo codes, address + payment + review + confirmation.
file-manager.html Cloud file browser: Tree sidebar, Toolbar, files Table, preview Sheet, storage ProgressRing.
calendar-app.html Calendar & scheduler: DatePicker, category chips, busy-hours ring, agenda Timeline, event detail Sheet.
docs-portal.html Help center / knowledge base: SearchBar, Tree categories, Markdown article, Rating, FAQ Accordion.

The full catalog with tag filters lives at docs/live-examples.html.


Project layout

.
├── src/                       # Library source
│   ├── parser/                # Lexer, parser, AST types
│   ├── runtime/               # Evaluator, reactive state, queries, actions,
│   │   ├── builtins.ts        #   builtin @-functions and action-step markers
│   │   ├── evaluator.ts       #   program planner + binding resolver
│   │   ├── state.ts           #   reactive store (subscribers, two-way binds)
│   │   ├── queries.ts         #   Query / Mutation registry + auto-refresh
│   │   ├── actions.ts         #   ActionRunner (@Run, @Set, @Reset, …)
│   │   ├── scripts.ts         #   ScriptRunner — useEffect-style Script(...)
│   │   └── router.ts          #   Hash-based router for Routes / NavLink
│   ├── library/               # Component specs and registry
│   │   └── components/        #   layout / content / forms / data / charts /
│   │                          #   chat / feedback / navigation / menu /
│   │                          #   patterns / scripts / router
│   ├── renderer/              # Tree → DOM
│   │   ├── renderer.ts        #   walks the tree, calls component renderers,
│   │   │                      #   tracks per-instance state by tree path
│   │   └── morph.ts           #   React-like DOM reconciler — keeps focus,
│   │                          #   selection, scroll, and <details>.open
│   ├── theme/                 # Token system + injected stylesheet
│   │   ├── index.ts           #   Seven built-in themes + custom token merge
│   │   └── styles.ts          #   Shadow-DOM stylesheet (theme-aware)
│   ├── prompt/                # System prompt generator
│   ├── language/              # Reusable language-support module
│   │   ├── grammar.ts         #   Pure-data tokens + CodeMirror StreamParser
│   │   ├── components.ts      #   Component catalog (derived from library)
│   │   ├── builtins.ts        #   @-builtin catalog (sourced from runtime)
│   │   ├── snippets.ts        #   Ready-to-insert templates for composites
│   │   └── index.ts           #   `getLanguageSpec()` barrel for editors
│   ├── element.ts             # The custom element
│   └── index.ts               # Public entry point
├── docs/                      # Static documentation site (HTML + CSS + JS)
├── _docs/                     # Internal design notes and inspirations (not shipped)
├── scripts/
│   ├── emit-prompt.mjs        # Writes dist/system_prompt*.txt from the bundle
│   └── build-docs.mjs         # Assembles ./site/ from docs/ + dist/
├── tests/                     # Vitest unit + element regression tests
├── dist/                      # Built artifacts (created by `npm run build`)
├── site/                      # Deployable static docs (created by `npm run build:docs`)
├── .github/workflows/         # GitHub Pages deploy pipeline
├── README.md                  # This file
└── coding-gen-skill.md        # Deep knowledge base for building full apps

Run it locally

Requirements: Node ≥ 18 and npm ≥ 9 (or pnpm/yarn — examples use npm).

Install

git clone https://github.com/asfand-dev/streaming-ui-script.git
cd streaming-ui-script
npm install

Build the library and system prompt

npm run build

Produces:

dist/streaming-ui-script.js          # ESM bundle
dist/streaming-ui-script.umd.cjs     # UMD bundle for older bundlers
dist/streaming-ui-script.iife.js     # IIFE for non-module <script> tags
dist/system_prompt.txt               # Full prompt — every component, JS, routing
dist/system_prompt_chat.txt          # Compact chat-focused prompt (lighter, OpenUI-Lang style)

The two prompt variants exist so host apps can pick the right flavour up front: system_prompt.txt (or getSystemPrompt() with no options) for rich generative UI surfaces that need JS and routing, and system_prompt_chat.txt (or getSystemPrompt({ mode: "chat" })) for chat assistants whose replies should stay purely declarative. Both are kept in lock-step with the library by the build script.

Run the test suite

npm test

Includes:

  • Parser / lexer correctness (tests/parser.test.ts).
  • Runtime evaluator + reactive state (tests/runtime.test.ts).
  • Built-in function semantics (covered across runtime + library tests).
  • JavaScript interactions: Script(...) lifecycle + @Js(...) (tests/javascript-integration.test.ts).
  • Hash-based router and Routes / Route / NavLink (tests/router.test.ts).
  • Theme resolution and token application (tests/theme.test.ts).
  • Component library smoke tests (tests/library.test.ts).
  • Element-level integration tests — Custom Elements + Shadow DOM via happy-dom (tests/element.test.ts).
  • System prompt generator output (tests/prompt.test.ts).
  • End-to-end demo programs from the docs (tests/demos.test.ts).
  • Language support spec for editor / tooling integrations (tests/language.test.ts).

Build the documentation site

npm run build:docs

Assembles ./site/ from ./docs/ + ./dist/. Serve it with anything static:

npx http-server site -p 4321
# or
npx serve site

Then open http://localhost:4321/index.html.


Security

The library treats every LLM-supplied attribute as untrusted and runs it through a small set of sanitisers before it lands on the DOM:

Sink Helper Effect
Anchor href (Link, BreadcrumbItem, NavbarItem, Markdown links) sanitiseHref Allow-lists http(s):, mailto:, tel:, fragments, root-relative paths. Rejects javascript:, vbscript:, data:text/html, control-char bypasses (java\tscript:), protocol-relative //host/.... Unsafe URLs collapse to #.
Image src (Image, Avatar, MediaCard, Hero, Testimonial, ChatBubble) sanitiseImageSrc Allow-lists http(s):, data:image/*, blob:, plus relative paths. Anything else falls back to an empty string so callers render a placeholder.
Inline style lengths (Container.maxWidth, Skeleton.height, …) sanitiseCssLength Restricts the alphabet so semicolons / quotes cannot inject extra declarations.
background-image: url(...) (Cover.imageSrc) sanitiseCssUrl Drops characters that would close the url() literal.
@OpenUrl("...") action step sanitiseHref (runtime) The action runner sanitises the URL before calling window.open (or any consumer onOpenUrl override). External windows are opened with noopener,noreferrer.

External links rendered by Link, NavbarItem, and the Markdown renderer get rel="noopener noreferrer" so the destination cannot read the opener's document.referrer.

If you embed <streaming-ui-script> behind a CSP, the bundle does not use eval. Script(...) bodies are evaluated with new (Async)Function(...) which requires 'unsafe-eval' if you want scripting to work; omit the attribute (and @Js action steps) if you cannot relax CSP.


CDN deployment

This repository ships its own copy of the bundle on GitHub Pages, so most users do not need to host anything themselves:

<script type="module" src="https://asfand-dev.github.io/streaming-ui-script/dist/streaming-ui-script.js"></script>
<streaming-ui-script theme="dark"></streaming-ui-script>

…and a fetch of system_prompt.txt server-side to build LLM messages:

curl https://asfand-dev.github.io/streaming-ui-script/dist/system_prompt.txt

To ship your own copy, run npm run build and serve the dist/ folder from any static host — every artifact in dist/ is self-contained.

GitHub Pages deployment for this repo is automated via .github/workflows/deploy-pages.yml. Push to main and the workflow will build, test, assemble site/, and publish.


Contributing

Contributions are very welcome. The fastest path is:

  1. Fork and clone the repo.
  2. npm install && npm test — make sure the suite is green on main first.
  3. Make your change in a focused branch (e.g. feat/inline-charts).
  4. Add or update tests in tests/. Aim for good edge-case coverage.
  5. Run npm run build to confirm the bundle and the system prompt still build.
  6. Open a pull request describing the motivation and any user-visible changes.

Issues, design discussions, and bug reports are tracked at https://github.com/asfand-dev/streaming-ui-script/issues.

By contributing you agree that your work will be released under the project's MIT license.


License

MIT — see LICENSE.

Top categories

Loading Svelte Themes