@openuidev/svelte-lang
v0.1.2
Published
Define component libraries, generate LLM system prompts, and render streaming OpenUI Lang output in Svelte 5 — the Svelte runtime for OpenUI generative UI
Maintainers
Readme
@openuidev/svelte-lang
Svelte 5 bindings for OpenUI Lang. Define model-renderable Svelte components, generate prompts from those definitions, and render streamed OpenUI Lang in a Svelte app.
Links: OpenUI Lang docs | GitHub repo
Install
npm install @openuidev/svelte-lang
# or
pnpm add @openuidev/svelte-langPeer dependencies: svelte >=5.0.0
Overview
@openuidev/svelte-lang brings the OpenUI Lang runtime to Svelte:
- Define Svelte components that a model is allowed to call, with Zod schemas for props.
- Generate prompts from the component library.
- Render streamed output with
<Renderer>as OpenUI Lang arrives.
Quick Start
1. Define a component
<!-- Greeting.svelte -->
<script lang="ts">
import type { ComponentRenderProps } from "@openuidev/svelte-lang";
let { props, renderNode }: ComponentRenderProps<{ name: string; mood?: "happy" | "excited" }> = $props();
</script>
<div class={props.mood === "excited" ? "text-xl font-bold" : ""}>
Hello, {props.name}!
</div>import { defineComponent } from "@openuidev/svelte-lang";
import { z } from "zod";
import Greeting from "./Greeting.svelte";
const GreetingDef = defineComponent({
name: "Greeting",
description: "Displays a greeting message",
props: z.object({
name: z.string().describe("The person's name"),
mood: z.enum(["happy", "excited"]).optional().describe("Tone of the greeting"),
}),
component: Greeting,
});2. Create a library
import { createLibrary } from "@openuidev/svelte-lang";
const library = createLibrary({
components: [GreetingDef, CardDef, TableDef /* ... */],
root: "Card", // optional default root component
});3. Generate a system prompt
const systemPrompt = library.prompt({
preamble: "You are a helpful assistant.",
additionalRules: ["Always greet the user by name."],
examples: ['User: Hi\n\nroot = Greeting("Alice", "happy")'],
});4. Render streamed output
<script lang="ts">
import { Renderer } from "@openuidev/svelte-lang";
import { library } from "$lib/library";
let { response, isStreaming }: { response: string | null; isStreaming: boolean } = $props();
</script>
<Renderer
{response}
{library}
{isStreaming}
onAction={(event) => console.log("Action:", event)}
/>API Reference
Component Definition
| Export | Description |
| :-------------------------- | :----------------------------------------------------------------------------------------- |
| defineComponent(config) | Define a single component with a name, Zod props schema, description, and Svelte component |
| createLibrary(definition) | Create a library from an array of defined components |
Rendering
| Export | Description |
| :--------- | :---------------------------------------------------------- |
| Renderer | Svelte component that parses and renders OpenUI Lang output |
RendererProps:
| Prop | Type | Description |
| :-------------- | :-------------------------------------- | :---------------------------------------------------------------- |
| response | string \| null | Raw OpenUI Lang text from the model |
| library | Library | Component library from createLibrary() |
| isStreaming | boolean | Whether the model is still streaming (disables form interactions) |
| onAction | (event: ActionEvent) => void | Callback when a component triggers an action |
| onStateUpdate | (state: Record<string, any>) => void | Callback when form field values change |
| initialState | Record<string, any> | Initial form state for hydration |
| onParseResult | (result: ParseResult \| null) => void | Callback when the parse result changes |
Children Rendering
Svelte components receive renderNode as a snippet prop (not via context). Use it to render child element nodes:
<script lang="ts">
import type { Snippet } from "svelte";
let { props, renderNode }: { props: { children?: unknown }; renderNode: Snippet<[unknown]> } = $props();
</script>
<div>
{#if props.children}
{@render renderNode(props.children)}
{/if}
</div>Parser (Server-Side)
| Export | Description |
| :------------------------------ | :----------------------------------------------------- |
| createParser(schema) | Create a one-shot parser for complete OpenUI Lang text |
| createStreamingParser(schema) | Create an incremental parser for streaming input |
The streaming parser exposes two methods:
| Method | Description |
| :------------ | :---------------------------------------------------- |
| push(chunk) | Feed the next chunk; returns the latest ParseResult |
| getResult() | Get the latest result without consuming new data |
After the stream ends, check meta.unresolved for any identifiers that were referenced but never defined. During streaming these are expected (forward refs) and are not treated as errors.
Errors
ParseResult.meta.errors contains structured OpenUIError objects. Each error has a type discriminant (currently always "validation") and a code for consumer-side filtering:
| Code | Meaning |
| :------------------ | :-------------------------------------------------- |
| missing-required | Required prop absent with no default |
| null-required | Required prop explicitly null with no default |
| unknown-component | Component name not found in the library schema |
| excess-args | More positional args passed than the schema defines |
Errors do not affect rendering. The parser stays permissive and renders what it can:
const result = parser.parse(output);
const critical = result.meta.errors.filter((e) => e.code === "unknown-component");Context Getters
Use these inside component renderers to interact with the rendering context:
| Function | Returns | Description |
| :------------------------- | :-------------------- | :----------------------------------------------------------------------------- |
| getOpenUIContext() | OpenUIContextValue | Access the full context object (library, streaming state, field accessors) |
| getIsStreaming() | () => boolean | Returns a getter for the streaming state. Call it reactively: isStreaming() |
| getTriggerAction() | Function | Trigger an action event |
| getGetFieldValue() | Function | Get a form field's current value |
| getSetFieldValue() | Function | Set a form field's value |
| getFormName() | string \| undefined | Get the current form's name |
| useSetDefaultValue(opts) | void | Set a field's default value once streaming completes |
Form Validation
| Export | Description |
| :--------------------------- | :---------------------------------------------------- |
| getFormValidation() | Access form validation state |
| createFormValidation() | Create a form validation context |
| setFormValidationContext() | Provide validation context to child components |
| validate(value, rules) | Run validation rules against a value |
| builtInValidators | Built-in validators (required, email, min, max, etc.) |
| parseRules(rules) | Parse a rules config object into ParsedRule[] |
Types
import type {
// Component definition
Library,
LibraryDefinition,
DefinedComponent,
ComponentRenderer,
ComponentRenderProps,
ComponentGroup,
SubComponentOf,
PromptOptions,
// Rendering
RendererProps,
OpenUIContextValue,
ActionConfig,
// Parser & core
ActionEvent,
ElementNode,
ParseResult,
LibraryJSONSchema,
// Validation
FormValidationContextValue,
ParsedRule,
ValidatorFn,
} from "@openuidev/svelte-lang";JSON Schema Output
Libraries can also produce a JSON Schema representation of their components:
const schema = library.toJSONSchema();
// schema.$defs["Card"] → { properties: {...}, required: [...] }
// schema.$defs["Greeting"] → { properties: {...}, required: [...] }Differences from React
| Concern | react-lang | svelte-lang |
| :----------------- | :----------------------------------------------- | :----------------------------------------------- |
| Children rendering | renderNode function prop returning ReactNode | renderNode snippet (Snippet<[unknown]>) |
| Context access | Hooks (useIsStreaming(), etc.) | Getters (getIsStreaming(), etc.) |
| Error boundaries | Class-based, preserves last valid render | svelte:boundary with auto-retry on prop change |
| Reactivity | Hooks re-run on every render | Runes ($state, $derived, $effect) |
