Integration

Drop-in setup for every framework.

<streaming-ui-script> is a standard Custom Element, so it works in every framework that renders HTML — React, Next.js, Vue, Nuxt, Angular, Svelte, Solid, Astro, Remix, and plain HTML.

Universal pattern

Three things matter no matter which framework you use:

  1. Import the bundle once in client-side code so it registers the custom element.
  2. Render <streaming-ui-script> wherever you want the LLM output to appear.
  3. Get a reference to the element and call setResponse(), appendChunk(), setTheme(), or setTools().

Pick your stack

Installation

Pick whichever channel suits your project:

# From npm — works for any bundler
npm install streaming-ui-script

# Or load from CDN — no build step required
<script type="module" src="https://asfand-dev.github.io/streaming-ui-script/dist/streaming-ui-script.js"></script>

The bundle is self-contained: it includes the parser, runtime, the complete component library, and the styles (rendered into the shadow DOM). You only ever need a single import.


React

React 18+Vite / CRA / Parcel

React (< 19) doesn't natively understand custom-element props or events, but it does pass attributes through and accepts a ref. The pattern below uses a ref to call methods and a useEffect hook to wire events.

// LLMResponse.tsx
import { useEffect, useRef } from "react";
import "streaming-ui-script";

interface Props {
  response: string;
  theme?: "light" | "dark" | "neon" | "pastel" | "glass" | "brutalist";
  onAssistantMessage?: (text: string) => void;
}

export function LLMResponse({ response, theme = "light", onAssistantMessage }: Props) {
  const ref = useRef<HTMLElement & { setResponse: (t: string) => void }>(null);

  useEffect(() => {
    ref.current?.setResponse(response);
  }, [response]);

  useEffect(() => {
    const el = ref.current;
    if (!el || !onAssistantMessage) return;
    const handler = (e: Event) => onAssistantMessage((e as CustomEvent).detail.message);
    el.addEventListener("assistant-message", handler);
    return () => el.removeEventListener("assistant-message", handler);
  }, [onAssistantMessage]);

  return <streaming-ui-script ref={ref} theme={theme} transparent />;
}

Add the JSX intrinsic element type once so TypeScript stops complaining:

// types/streaming-ui-script.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from "react";
declare global {
  namespace JSX {
    interface IntrinsicElements {
      "streaming-ui-script": DetailedHTMLProps<
        HTMLAttributes<HTMLElement> & {
          theme?: string;
          transparent?: boolean;
          showerrors?: string;
        },
        HTMLElement
      >;
    }
  }
}

For React 19+ you can pass props directly as element properties — no ref needed for the simple read-only case:

<streaming-ui-script theme="dark" response={text} />

Next.js (app router)

Next 13+app routerClient component

Custom elements only work in the browser, so the wrapper must be a client component (the import statement registers the element on HTMLElement globals). Mark the file with "use client" at the top.

// app/components/LLMResponse.tsx
"use client";

import { useEffect, useRef } from "react";
import "streaming-ui-script";

export function LLMResponse({ response, theme = "light" }: { response: string; theme?: string }) {
  const ref = useRef<any>(null);
  useEffect(() => { ref.current?.setResponse(response); }, [response]);
  return <streaming-ui-script ref={ref} theme={theme} transparent />;
}

// app/page.tsx — server component
import { LLMResponse } from "./components/LLMResponse";

export default function Page() {
  const program = `root = Stack([greeting])
greeting = Card([CardHeader("Hello", "Generative UI in Next.js")])`;
  return <LLMResponse response={program} theme="light" />;
}

For SSR-only pages you can skip the wrapper and import via a next/dynamic call with ssr: false:

const LLMResponse = dynamic(() => import("./components/LLMResponse").then(m => m.LLMResponse), {
  ssr: false,
});

Vue 3

Vue 3Vitecomposition API

Tell Vue to skip its component resolver for any tag matching the custom element. In vite.config.ts (or wherever the Vue plugin is configured):

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue({
      template: { compilerOptions: { isCustomElement: (tag) => tag === "streaming-ui-script" } },
    }),
  ],
});

Then use it like any other element:

<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
import "streaming-ui-script";

const target = ref<HTMLElement | null>(null);
const response = ref(`root = Stack([Card([CardHeader("Hello from Vue")])])`);
const theme = ref("light");

onMounted(() => (target.value as any)?.setResponse(response.value));
watch(response, (next) => (target.value as any)?.setResponse(next));
</script>

<template>
  <streaming-ui-script ref="target" :theme="theme" transparent />
</template>

Nuxt 3

Nuxt 3SSR

Nuxt renders on the server by default, so the import has to be deferred until the client. Use a <ClientOnly> wrapper and register the element type in nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  vue: {
    compilerOptions: { isCustomElement: (tag) => tag === "streaming-ui-script" },
  },
});

// plugins/streaming-ui-script.client.ts
import "streaming-ui-script";
export default defineNuxtPlugin(() => {});
<!-- pages/index.vue -->
<template>
  <ClientOnly>
    <streaming-ui-script theme="light" transparent ref="renderer" />
  </ClientOnly>
</template>

Angular

Angular 17+standalone components

Add CUSTOM_ELEMENTS_SCHEMA to the component (or module) so the template compiler accepts the unknown tag, then import the bundle once in main.ts:

// main.ts
import "streaming-ui-script";
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";

bootstrapApplication(AppComponent);

// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild, AfterViewInit } from "@angular/core";

@Component({
  standalone: true,
  selector: "app-root",
  template: `<streaming-ui-script #renderer theme="light" transparent></streaming-ui-script>`,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent implements AfterViewInit {
  @ViewChild("renderer") renderer!: ElementRef<HTMLElement & { setResponse(t: string): void }>;

  ngAfterViewInit() {
    this.renderer.nativeElement.setResponse(`root = Stack([Card([CardHeader("Hello, Angular")])])`);
  }
}

Svelte / SvelteKit

Svelte 4 / 5SvelteKit

Svelte already understands custom elements. Import the bundle (in a client-side +page.ts hook or directly in the component) and you're done.

<!-- routes/+page.svelte -->
<script lang="ts">
  import { onMount } from "svelte";
  import "streaming-ui-script";

  let renderer: any;
  const program = `root = Stack([Card([CardHeader("Hello from Svelte")])])`;
  onMount(() => renderer?.setResponse(program));
</script>

<streaming-ui-script bind:this={renderer} theme="light" transparent />

For server-rendered routes wrap the import in browser-guarded code (e.g. if (browser) await import("streaming-ui-script");) so it only runs in the client bundle.


SolidJS

SolidJS 1.x
// App.tsx
import { onMount } from "solid-js";
import "streaming-ui-script";

export default function App() {
  let renderer: any;
  onMount(() => renderer?.setResponse(`root = Stack([Card([CardHeader("Hello, Solid")])])`));
  return <streaming-ui-script ref={renderer} theme="light" transparent />;
}

Astro

Astro 4+

Astro can ship the element either as a hydrated island or as a static tag with a small inline script. Both approaches are valid:

---
// src/pages/index.astro
---
<streaming-ui-script id="response" theme="light" transparent></streaming-ui-script>

<script>
  import "streaming-ui-script";
  const el = document.getElementById("response") as any;
  el.setResponse(`root = Stack([Card([CardHeader("Hello, Astro")])])`);
</script>

Plain HTML

No build stepWorks offline

The CDN build is the smallest possible footprint — no bundler, no framework, just one script tag.

<!doctype html>
<html>
  <body>
    <streaming-ui-script theme="light" transparent></streaming-ui-script>

    <script type="module">
      import "https://asfand-dev.github.io/streaming-ui-script/dist/streaming-ui-script.js";
      const el = document.querySelector("streaming-ui-script");
      el.setResponse(`root = Stack([Card([CardHeader("Hello, world")])])`);
    </script>
  </body>
</html>

Common patterns across frameworks

Streaming an LLM response

Set streaming to true, push tokens with appendChunk(), then flip the attribute back when the stream ends. Internally the renderer suppresses transient parse errors while the attribute is set.

el.streaming = true;
el.clear();
for await (const chunk of stream) el.appendChunk(chunk);
el.streaming = false;

Wiring tools (Query / Mutation)

Tools are async functions with a single argument object. The runtime invokes them whenever a Query or Mutation references them.

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

Reacting to follow-up clicks

Buttons that call @ToAssistant("...") dispatch an assistant-message event with event.detail.message. Hook it into your chat input.

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

Aligning the background with your page

When the surrounding page already has a custom theme, pass the transparent attribute (used in every snippet above) so the host inherits the parent's background. The internal cards retain their own surface colors. See the theming page for two more options.

Generating the system prompt

Either fetch the bundled file or build it programmatically — both return identical text:

// 1. Fetch the canned prompt
const promptText = await fetch("https://asfand-dev.github.io/streaming-ui-script/dist/system_prompt.txt").then(r => r.text());

// 2. Generate at runtime (lets you add tools or extra rules)
import { SYSTEM_PROMPT_TEXT, generatePrompt, defaultLibrary } from "streaming-ui-script";
const same = SYSTEM_PROMPT_TEXT;
const custom = generatePrompt(defaultLibrary, {
  preamble: "You are an analytics assistant.",
  additionalRules: ["Always end with a FollowUpBlock."],
});

Need a different framework?

The element relies only on the Custom Elements v1 and ES2020 features. Anywhere you can render HTML and run JavaScript in a modern browser, the bundle just works.