instio
v0.1.4
Published
The Instio ecosystem — a declarative UI framework for building interactive apps in JavaScript — inspired by Gradio.
Maintainers
Readme
🧵 Instio
A declarative UI DSL for JavaScript — build interactive browser apps with pure functions.
Instio lets you describe your interface and event logic in a single app.js file. Inspired by Gradio, it compiles everything into a fully client-side SPA — no server round-trips, no serialisation. Your handler functions run directly in the browser.
npm create instio@latest my-appcd my-app
npm install
npm run dev # → http://localhost:7860import { instio } from "@instio/core";
async function* generate(prompt) {
const words = `You asked: "${prompt}". Here is a response.`.split(" ");
for (const word of words) {
yield word + " ";
await new Promise((r) => setTimeout(r, 40));
}
}
const app = instio((ui) => {
const prompt = ui.textbox({ placeholder: "Ask anything…", lines: 1 });
const chatbot = ui.chatbot({ height: 420 });
ui.button({ label: "Send", variant: "primary" })
.stream(generate)
.from(prompt)
.to(chatbot);
});
export default app;Table of Contents
- Why Instio?
- The Instio Ecosystem
- How It Works
- Quick Start
- Writing an App
- Events
- Component Library
- State & Shared Logic
- Assets & Branding
- Theming & Deployment
- Examples
- Package Reference
- Contributing
- License
Why Instio?
Instio brings Gradio-style ergonomics to JavaScript — declarative inputs, outputs, buttons, layout blocks, and streaming chat — while keeping your entire app deployable as static files.
| | Typical Gradio-style stack | Instio |
|---|---|---|
| UI definition | Server-side blocks / config | Pure JavaScript DSL in app.js |
| Event handlers | Run on a Python (or Node) server | Bundled into the browser |
| Deployment | Requires a running backend | Static HTML + JS (Netlify, Vercel, S3, CDN, …) |
| Streaming | Server push / websockets | Async generators in the client |
| Extensibility | Custom components in one runtime | Renderer-agnostic core + pluggable renderers |
Instio is a good fit when you want:
- A demo, prototype, or internal tool with form inputs and live outputs
- A chat UI that streams tokens from an LLM API via
fetch() - A calculator, dashboard, or widget gallery without maintaining a separate frontend framework boilerplate
- Something you can ship as a static site and open in any browser
Handlers stay plain JavaScript. Sync functions, async/await, and async function* generators all work. If you need an external API, call it yourself — Instio does not proxy your events through a backend.
The Instio Ecosystem
Instio is split into focused packages that work together. You typically install all three when starting a new project; advanced users can depend on @instio/core alone when building a custom renderer.
┌─────────────────────────────────────────────────────────────────┐
│ your app.js │
│ import { instio } from "@instio/core" │
│ export default instio((ui) => { … }) │
└────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ @instio/core│ │@instio/svelte│ │ instio │
│ DSL engine │───▶│ renderer │◀───│ CLI + tmpl │
│ no DOM dep │ │ Vite + SPA │ │ npm create │
└─────────────┘ └──────────────┘ └─────────────┘@instio/core — DSL engine
The heart of Instio. Provides:
instio()— declare your UI tree and wire eventsui.*components — textbox, number, button, row, column, chatbot, …- Event model —
.click(),.change(),.submit(),.stream() - Renderer-agnostic output —
{ tree, events, handlers, state }
No DOM, no bundler, no framework dependency. Use it anywhere you need to compile an Instio app description.
npm install @instio/core@instio/svelte — Svelte renderer
The official browser renderer. Takes your app.js, bundles it with Vite, and mounts a Svelte SPA where:
- Every component gets a reactive store keyed by node id
EventRunnercalls your handler functions directly in the browser- Streaming generators update outputs on each animation frame
- A built-in dark theme and component library ship out of the box
npm install @instio/svelteScaffolded apps call appRuntime() from this package to start dev and production builds.
instio — CLI & project template (this package)
The front door to Instio. One command scaffolds a working project with @instio/core and @instio/svelte pre-wired:
npm create instio@latest my-app
# also: pnpm create instio | yarn create instioThe default template includes a calculator demo in src/app.js — a practical reference for grid layout, button variants, and shared handler state.
| Package | npm | Role |
|---------|-----|------|
| @instio/core | | DSL —
instio(), components, events, state |
| @instio/svelte | | Svelte renderer — Vite SPA, client-side execution |
|
instio | | CLI scaffolder —
npm create instio@latest <app> |
Source code and deep-dive docs for each package live on GitHub:
How It Works
src/app.js your UI tree + handler functions
│
▼ @instio/svelte bundles app.js at build time (Vite)
dist/ static HTML + JS + CSS
│
▼ open in any browser
User action → EventRunner calls handler() → output stores update instantlyinstio((ui) => { … })builds a node tree, event bindings, and a livehandlersmap.@instio/svelteimports yourapp.jsand renders it with Svelte components.- On every click, change, or submit,
EventRunnerreads input stores, calls your function, writes output stores — all client-side.
The default export shape consumed by the renderer:
{ tree, events, handlers, state }External services (LLMs, REST APIs, WebSockets) are called from your handler code — Instio never sits in the middle.
Quick Start
Prerequisites
| Tool | Version | |------|---------| | Node.js | ≥ 18.0.0 |
Create a new app
npm create instio@latest my-app
cd my-app
npm install
npm run dev # → http://localhost:7860Edit src/app.js — the dev server hot-reloads on save.
Build & serve
npm run build # output → dist/
npm run serve # serve dist/ locallyUpload dist/ to any static host. No Node.js server is required at runtime.
Scaffolded project layout
my-app/
├── index.js dev/build entry — calls appRuntime() from @instio/svelte
├── package.json
├── README.md
├── .gitignore
├── dist/ build output (created by npm run build)
└── src/
├── app.js your Instio app — start here
└── assets/
└── images/ static images (referenced by key in branding)App scripts
| Command | Description |
|---------|-------------|
| npm run dev | Hot-reloading Vite dev server at http://localhost:7860 |
| npm run build | Production static build written to dist/ |
| npm run serve | Serve dist/ with npx serve |
CLI app name rules
App names must start with a lowercase letter or digit and contain only lowercase letters, numbers, hyphens, and underscores.
Valid: my-chat-app, llm_demo, app1
Invalid: MyApp, my app, .hidden
Writing an App
Every Instio app follows the same three-step pattern:
import { instio } from "@instio/core";
// 1. Plain JS handlers — sync, async, or async generators
function greet(name) {
return `Hello, ${name}!`;
}
// 2. Declare UI + wire events
const app = instio({
branding: {
name: "My App",
logo: { key: "logo", alt: "Logo", lazy: true },
}
}, (ui) => {
const name = ui.textbox({ label: "Name", placeholder: "World" });
const output = ui.markdown({ value: "" });
ui.button({ label: "Greet", variant: "primary" })
.click(greet, [name], [output]);
});
// 3. Export for the renderer
export default app;Layout blocks
Nest components inside rows and columns:
ui.row(() => {
ui.column(() => {
ui.number({ label: "A", value: 0 });
});
ui.column(() => {
ui.number({ label: "B", value: 0 });
}, { scale: 2 }); // twice as wide as sibling columns
});Wiring inputs to outputs
Component handles returned by ui.* calls are wired explicitly:
const a = ui.number({ label: "A", value: 0 });
const b = ui.number({ label: "B", value: 0 });
const out = ui.number({ label: "Result", readonly: true });
ui.button({ label: "Add", variant: "primary" })
.click((x, y) => Number(x) + Number(y), [a, b], [out]);Seamless Groups
You can group form inputs together and strip out their inner spacing and border-radii using the seamless property on a group, creating a single unified block:
ui.group(() => {
ui.textbox({ placeholder: "Search..." });
ui.button({ label: "Go" });
}, { seamless: true });Events
Every interactive component handle exposes a fluent event API.
Click / Change / Submit
handle.click(handler, [inputs], [outputs]);
handle.change(handler, [inputs], [outputs]);
handle.submit(handler, [inputs], [outputs]);Handlers receive current input values in order and return an output value, or an array for multiple outputs:
function add(a, b) {
return Number(a) + Number(b);
}
function split(text) {
return [text.toUpperCase(), text.length];
}
function reset() {
return "0";
}
btn.click(reset, [], [display]); // no inputs neededDynamic Property Updates
Handlers can return instio.update() to dynamically change a component's properties (like visibility, label, or variant) in addition to its value, instead of just returning raw values:
import { instio, update } from "@instio/core";
function submit() {
return [
update({ value: "Saved!", label: "Done" }),
update({ visible: false })
];
}Errors & Toast Notifications
If a handler throws an error, Instio automatically catches it, halts execution, and displays a sliding Toast Notification at the bottom right of the screen. The error is also logged to the console.
Automatic Loading States
When an event starts, Instio automatically overlays a .tio-loading spinner and blur effect on all components marked as outputs. This prevents user interaction until the handler completes.
Streaming (async generators)
Use .stream() for progressive / token-by-token output:
ui.button({ label: "Generate", variant: "primary" })
.stream(asyncGenerator)
.from(prompt)
.to(chatbot);Each yield updates the output store immediately — ideal for LLM streaming:
async function* generate(prompt) {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o",
stream: true,
messages: [{ role: "user", content: prompt }],
}),
});
for await (const chunk of parseSSE(res.body)) yield chunk;
}Simulated streaming without an API:
async function* generate(prompt) {
const words = `You asked: "${prompt}".`.split(" ");
for (const word of words) {
yield word + " ";
await new Promise((r) => setTimeout(r, 50));
}
}Component Library
Components below are registered in @instio/core and rendered by @instio/svelte.
Inputs
| Component | DSL call | Key props |
|-----------|----------|-----------|
| Text box | ui.textbox({ … }) | label, placeholder, value, lines, readonly |
| Number | ui.number({ … }) | label, value, minimum, maximum, step, readonly |
| Slider | ui.slider({ … }) | label, minimum, maximum, step, value |
| Dropdown | ui.dropdown({ … }) | label, choices, value, multiselect |
| Image | ui.image({ … }) | label, type, sources, height, width |
Outputs & Interactive
| Component | DSL call | Key props |
|-----------|----------|-----------|
| Markdown | ui.markdown({ … }) | value, sanitize_html |
| JSON | ui.json({ … }) | label, value |
| Dataframe | ui.dataframe({ … }) | label, value, headers |
| Checkbox | ui.checkbox({ … }) | label, value |
| Checkbox Group | ui.checkboxgroup({ … }) | label, value, choices |
| Toggle | ui.toggle({ … }) | label, value |
| Datetime | ui.datetime({ … }) | label, value, type |
| File Upload | ui.file({ … }) | label, value, accept |
| Image Gallery | ui.gallery({ … }) | label, value |
| Progress Bar | ui.progress({ … }) | label, value |
| Chatbot | ui.chatbot({ … }) | label, value, height, show_copy_button |
| Button | ui.button({ … }) | label, variant (primary | secondary | stop) |
ui.clear_button() and ui.submit_button() are aliases of ui.button().
Button variants map to visual styles in the Svelte renderer:
| Variant | Typical use |
|---------|-------------|
| primary | Main actions, operators, submit |
| secondary | Neutral actions, number keys |
| stop | Clear, cancel, destructive actions |
Layout & Chrome
| Block | DSL call | Key props |
|-------|----------|-----------|
| Row | ui.row(() => { … }) | equal_height, variant |
| Column | ui.column(() => { … }) | scale, min_width, variant |
The Svelte renderer also includes components like
Checkbox,Radio,Tab, andAccordion. These are not yet exposed in the core DSL — follow the @instio/core contributor guide to add new component schemas.
State & Shared Logic
Root state object
For persistent, cross-handler state, use the state argument passed to instio():
const app = instio((ui, state) => {
state.set({ count: 0 });
const counter = ui.number({ label: "Count", value: 0 });
counter.bind(state);
ui.button({ label: "Increment" }).click(() => {
state.set({ count: state.get().count + 1 });
});
});| Method | Description |
|--------|-------------|
| state.get() | Snapshot of current state |
| state.set(partial) | Merge update and notify subscribers |
| state.subscribe(fn) | Subscribe to changes; returns unsubscribe |
Module-level closures
For app-specific logic (calculator memory, multi-step wizards, game state), module-level variables work naturally — handlers are real functions bundled into the browser, not serialised strings:
const calc = { prev: null, op: null, resetNext: false };
function pressOperator(symbol) {
return (display) => {
calc.prev = parseFloat(display);
calc.op = symbol;
calc.resetNext = true;
return display;
};
}Assets & Branding
Place images in src/assets/images/ and reference them by key in branding:
// Configured in instio() options:
const app = instio({
branding: {
name: "My App",
logo: { key: "logo", alt: "Logo", lazy: true },
}
}, (ui) => {
// ...
});
// resolves src/assets/images/logo.svg at build timeThe Svelte renderer indexes your asset folder via Vite and supports variant paths and density suffixes (logo@2x).
Theming & Deployment
Built-in Themes
Instio comes with several built-in themes that dramatically alter the visual appearance of your components out of the box.
You can set the theme when initializing your app:
const app = instio({ theme: "skeuomorphic" }, (ui) => { ... });Available themes:
default: The classic Instio orange/neutral flat design.base: A minimal, grayscale foundation theme.origin: Vibrant orange, modeled after Gradio 4's appearance.citrus: Amber/yellow theme with playful 3D button effects.monochrome: High-contrast black and white, newspaper-like aesthetic.soft: A gentle, pastel indigo theme with heavily rounded corners.glass: Frosted cyan gradients with glassy blur effects (backdrop-filter).ocean: Deep blue and aquatic teal gradients.skeuomorphic: A deeply authentic, classic early-2000s glossy UI with inset shadows, realistic gradients, and tactile controls.
Customise the look
Global CSS custom properties are defined in @instio/svelte's theme. Override them in your own stylesheet:
:root {
--brand-500: #6366f1;
--brand-600: #4f46e5;
--bg-app: #0f0f13;
--bg-panel: #18181f;
--bg-surface: #22222d;
--text-primary: #f1f1f3;
--radius-sm: 6px;
}See the @instio/svelte theming docs for the full token list.
Deploy anywhere
npm run buildUpload the contents of dist/ to Netlify, Vercel, Cloudflare Pages, S3, GitHub Pages, or any static host. Your handlers, UI, and assets are self-contained — no Instio server required in production.
Examples
The scaffolded template ships with a calculator in src/app.js — a full demo of:
ui.row()/ui.column()grid layout withscalefor wide keys- Readonly
ui.textbox()as a display - Operator chaining, modulo, percent, and sign toggle via closure state
Other reference patterns:
Simple adder
import { instio } from "@instio/core";
function add(a, b) {
return Number(a) + Number(b);
}
const app = instio((ui) => {
const a = ui.number({ label: "A", value: 0 });
const b = ui.number({ label: "B", value: 0 });
const out = ui.number({ label: "Result", readonly: true });
ui.button({ label: "Add", variant: "primary" }).click(add, [a, b], [out]);
});
export default app;Streaming chat
import { instio } from "@instio/core";
async function* generate(prompt) {
const words = `You asked: "${prompt}". Here is a thoughtful response.`.split(" ");
for (const word of words) {
yield word + " ";
await new Promise((r) => setTimeout(r, 50));
}
}
const app = instio((ui) => {
ui.markdown({ value: "# 🧵 Instio Chat" });
const chatbot = ui.chatbot({ label: "Chat", height: 480 });
const prompt = ui.textbox({ placeholder: "Type a message…", lines: 1 });
ui.button({ label: "Send", variant: "primary" })
.stream(generate)
.from(prompt)
.to(chatbot);
});
export default app;More examples in the GitHub repository: basic.js · chat.js
Package Reference
When to use which package
| Goal | Install |
|------|---------|
| Start a new Instio app | npm create instio@latest my-app |
| Add Instio to an existing bundler setup | @instio/core + @instio/svelte |
| Build a custom renderer (React, Vue, …) | @instio/core only |
| Run dev/build in a scaffolded app | Already included via @instio/svelte |
Manual setup (without the CLI)
npm install @instio/core @instio/svelteCreate src/app.js with your instio() definition, then wire @instio/svelte's appRuntime() in an entry file — the scaffolded template shows the exact layout.
Building a custom renderer
@instio/core is intentionally DOM-free. To build your own renderer:
- Import the compiled app (
tree,events,handlers). - Walk
treerecursively — render leaf nodes and layout blocks. - Maintain a value store per node id.
- On user interaction, call handlers with collected input values and write outputs.
Reference implementation: @instio/svelte NodeRenderer + EventRunner.
Contributing
Contributions are welcome across the entire Instio ecosystem — core DSL, Svelte renderer, CLI template, docs, and new component types.
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes and add tests where appropriate
- Open a pull request
License
MIT © Instio Contributors
