Svelte is the language. Rust is the kernel. Your app owns every pixel.
Early Development — RVST is an active research project, not production-ready software. APIs will change. Features are incomplete. If you're building something that needs to ship today, use Tauri or Electron. If you want to explore what native Svelte rendering could look like, read on.
Quick Start | How It Works | Native APIs | Templates | Configuration
RVST is a new execution target for Svelte. Write components with Svelte 5, style with Tailwind or scoped CSS, and ship a native desktop app. No Electron. No webview. RVST replaces the browser engine entirely with a purpose-built Rust rendering stack.
Your Svelte code compiles to JavaScript as usual. RVST executes it in an embedded QuickJS runtime, maps the component tree to a Rust layout engine (Taffy), renders with a GPU vector graphics engine (Vello), and displays in a native window (winit). The result is a desktop app that starts instantly, uses minimal memory, and renders at native quality.
What you get:
npm install -g @rvst/cli
This installs the rvst command globally. It downloads the correct Rust binary for your platform automatically.
rvst create my-app
cd my-app
npm install
<!-- src/App.svelte -->
<script>
let count = $state(0);
</script>
<div class="app">
<h1>Hello from RVST</h1>
<button onclick={() => count++}>
Clicked {count} times
</button>
</div>
<style>
.app {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
font-family: system-ui;
}
button {
padding: 8px 16px;
border-radius: 6px;
background: #3b82f6;
color: white;
border: none;
font-size: 14px;
cursor: pointer;
}
</style>
// src/entry.js
import { mount } from 'svelte';
import App from './App.svelte';
export function rvst_mount(target) {
return mount(App, { target });
}
export default rvst_mount;
// vite.config.js
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { rvstPlugin } from '@rvst/vite-plugin';
export default defineConfig({
plugins: [rvstPlugin(), svelte()],
build: {
outDir: 'dist',
target: 'esnext',
lib: {
entry: 'src/entry.js',
formats: ['es'],
fileName: 'app',
},
},
});
rvst build
rvst run
Or with watch mode for development:
rvst dev
Svelte Component (.svelte)
|
v
Vite + vite-plugin-rvst
(compiles to JS bundle)
|
v
QuickJS Runtime (executes JS)
|
v
DOM Stubs (translate DOM ops to Rust ops)
|
v
rvst-tree (DOM-like tree in Rust)
|
v
lightningcss (CSS parsing + cascade + var() resolution)
|
v
Taffy (CSS Flexbox/Grid layout)
|
v
Vello + wgpu (GPU vector rendering)
|
v
winit (native window)
RVST intercepts Svelte's compiled DOM operations at the lowest level. When Svelte calls createElement, setAttribute, or insertBefore, these become Rust ops that build a tree. CSS is parsed by lightningcss with full cascade, specificity, custom property inheritance, and @media query evaluation. Taffy computes layout. Vello renders to the GPU.
The entire pipeline runs on the main thread with sub-millisecond frame times for typical UIs.
RVST exposes native platform capabilities to Svelte via globalThis.__rvst:
<script>
const rvst = globalThis.__rvst;
// Custom titlebar (remove OS chrome)
$effect(() => rvst?.disableDecorations());
</script>
<div onmousedown={() => rvst?.startDragging()}>
<!-- custom titlebar content -->
<button onclick={() => rvst?.minimize()}>-</button>
<button onclick={() => rvst?.maximize()}>+</button>
<button onclick={() => rvst?.close()}>x</button>
</div>
<script>
const fs = globalThis.__rvst?.fs;
async function loadConfig() {
const text = fs.readText('/path/to/config.json');
return JSON.parse(text);
}
function saveConfig(data) {
fs.writeText('/path/to/config.json', JSON.stringify(data, null, 2));
}
</script>
| API | Description |
|---|---|
__rvst.disableDecorations() |
Remove OS window chrome |
__rvst.enableDecorations() |
Restore OS window chrome |
__rvst.startDragging() |
Begin window drag (call from mousedown) |
__rvst.minimize() |
Minimize window |
__rvst.maximize() |
Maximize/restore window |
__rvst.close() |
Close window |
__rvst.fs.readText(path) |
Read file as string |
__rvst.fs.writeText(path, content) |
Write string to file |
rvst create <name> [-t template] Scaffold a new project
rvst dev Build + watch for changes
rvst build Build the Svelte bundle
rvst run [file.js] [file.css] Run the desktop app
rvst snapshot [file.js] Dump scene graph as JSON
rvst a11y [file.js] Dump accessibility tree
rvst ascii [file.js] Semantic tree dump (default)
rvst --ascii=tree:css [file.js] Tree with CSS classes + properties
rvst --ascii=structure [file.js] Box-drawing layout map
rvst --ascii=validate [file.js] Cross-validate tree vs pixels
rvst --filter="role:button" Filter tree output
rvst analyze [CATEGORY] [file.js] Run scene analysis
diagnostics Zero-size, offscreen, overlap, no-handler
layout Depth, utilization, whitespace, flex stats
a11y Unlabeled buttons, missing handlers, roles
contrast WCAG 2.1 AA/AAA contrast ratios
heatmap Node density truecolor heatmap
all Run all analyzers
rvst --version Show version
Place .ttf or .otf files in a fonts/ directory next to your bundle. RVST auto-loads them at startup.
dist/
app.js
app.css
fonts/
Phosphor.ttf # icon font
Inter-Variable.ttf # custom text font
Use in CSS:
.icon { font-family: "Phosphor"; font-size: 16px; }
.heading { font-family: "Inter"; font-weight: 600; }
RVST's CSS engine (powered by lightningcss) supports:
!important overridevar(--theme-bg))@media queries (min-width, max-width, min-height, max-height)@layer (Tailwind v4):not(), :first-child, :last-child, :nth-child:hover, :focus, :active>), adjacent (+), sibling (~)calc() with rem, em, px, vw, vh, %linear-gradient() backgroundstransform: translate, rotate, scale, skewtext-decoration: underline, line-through, overlineborder-radius, box-shadow (multiple), opacityrvst create my-app # default — counter with scoped CSS
rvst create my-app -t tailwind # Tailwind v4 + utility classes
rvst create my-app -t dashboard # custom titlebar, routing, dark/light theme, icons
rvst create my-app -t shadcn # Tailwind + bits-ui component primitives
packages/rvst/
crates/
rvst-core/ Protocol layer (NodeId, Op, Rect)
rvst-tree/ DOM-like tree with event handlers
rvst-text/ Text shaping (Parley) + font metrics (skrifa)
rvst-quickjs/ QuickJS runtime + DOM stubs for Svelte 5
rvst-shell/ Layout (Taffy) + rendering (Vello) + windowing (winit)
rvst-render-wgpu/ GPU rendering backend
js/
vite-plugin-rvst/ Vite plugin (redirects Svelte internals to RVST bridge)
renderer-bridge-js/ DOM operation bridge (Svelte → RVST ops)
| Platform | Status | Notes |
|---|---|---|
| macOS (Apple Silicon) | Stable | Primary development platform. Metal backend. |
| macOS (Intel) | Stable | Metal backend. |
| Linux (X11/Wayland) | Supported | Vulkan backend. Install vulkan-loader and GPU drivers. |
| Windows 10/11 | Supported | DX12 backend (Vulkan fallback). |
| Headless/CI | Supported | Software rendering via LLVMpipe/SwiftShader. |
# Ubuntu/Debian
sudo apt install build-essential pkg-config libvulkan-dev libwayland-dev
# Fedora
sudo dnf install vulkan-loader-devel wayland-devel
No additional dependencies. wgpu uses DX12 natively. Ensure GPU drivers are up to date.
RVST can render without a window for testing, CI, or server-side rendering:
# Dump scene graph
rvst --snapshot dist/app.js | jq '.nodes | length'
# Dump accessibility tree
rvst --a11y dist/app.js | jq '.[] | select(.role == "button")'
RVST can visualize UI state as text — useful for AI agents, debugging, and CI validation. All examples below are from the same dashboard app:
Dashboard app — traffic lights, sidebar nav with icons, stat cards, activity feed
Structure — box-drawing layout map showing element boundaries and nesting:
rvst --ascii=structure dist/app.js
Render — pixel-sampled ASCII art of the actual GPU output:
rvst --ascii=render dist/app.js
Overlay — pixel background with semantic labels overlaid:
rvst --ascii=overlay dist/app.js
Validate — cross-checks tree against pixels, marks mismatches with !:
rvst --ascii=validate dist/app.js
Semantic tree (default) — compact, agent-friendly:
rvst --ascii dist/app.js
Tree with CSS — classes and key computed properties:
rvst --ascii=tree:css dist/app.js
Tree with layout — computed rects (position + size):
rvst --ascii=tree:layout dist/app.js
Full tree — role + classes + rects combined:
rvst --ascii=tree:full dist/app.js
Filter the tree to focus on specific elements:
# Show only buttons
rvst --ascii=tree --filter="role:button" dist/app.js
# Find elements with a CSS class
rvst --ascii=tree:css --filter="class:bg-red" dist/app.js
# Combine filters with +
rvst --ascii=tree --filter="role:button+has:handler" dist/app.js
RVST includes built-in analyzers that inspect your UI for layout issues, accessibility gaps, and contrast problems. Each produces a colored terminal report.
Surfaces layout anomalies automatically detected during rendering — zero-size nodes with content, offscreen elements, sibling overlap >50%, and buttons without event handlers:
rvst analyze diagnostics dist/app.js
Quantifies your UI's spatial characteristics — node count, nesting depth, viewport utilization, whitespace ratio, and flex direction distribution:
rvst analyze layout dist/app.js
In this dashboard: 245 nodes, max depth 7, only 23.5% viewport utilization — most content is in the center, leaving the bottom half empty.
Audits interactive elements for semantic completeness — buttons without accessible names, interactive elements without handlers, role distribution:
rvst analyze a11y dist/app.js
Samples actual rendered pixels behind each text node and computes contrast ratios against WCAG AA (4.5:1) and AAA (7:1) thresholds:
rvst analyze contrast dist/app.js
Shows actual foreground/background colors sampled from the GPU render — not CSS values, but what the user sees.
Visualizes where UI elements cluster in the viewport as a truecolor terminal heatmap:
rvst analyze heatmap dist/app.js
Cold (blue) = empty space, hot (red) = many overlapping elements. In this dashboard, the sidebar and stat cards are the densest regions.
rvst analyze all dist/app.js
RVST includes a windowed test harness that opens a real GPU-rendered window and accepts JSON commands via stdin. Built for AI agents, CI pipelines, and interactive debugging.
rvst test launch dist/app.js
The app opens in a real window. Send JSON commands on stdin, get JSON responses on stdout — one line per command, one line per response. Every interaction automatically diffs the scene and runs lints.
# What's rendered right now?
> {"cmd": "snapshot"}
< {"node_count": 245, "viewport_w": 1024, "viewport_h": 768}
# Find all buttons
> {"cmd": "find", "role": "button"}
< {"count": 19, "nodes": [{"id": 152, "name": "Overview", ...}, ...]}
# Get full diagnostic on one node
> {"cmd": "explain", "id": 134}
< {"layout": {...}, "visibility": {...}, "styles": {...}, "clip_chain": [...]}
# Why can't I see this element?
> {"cmd": "why_not_visible", "id": 500}
< {"visible": false, "reasons": ["ClippedByAncestor(134)"]}
Every interaction command automatically snapshots before and after, diffs the changes, and runs lints. You get the full picture in one response:
> {"cmd": "click", "text": "Settings"}
< {
"ok": true,
"clicked": "Settings",
"changes": {"total": 160, "styles": 6, "added": 147, "removed": 7},
"lints": [
{"level": "info", "lint": "bulk_change", "message": "160 changes detected"}
]
}
Click by text or position. Scroll, type, navigate with tab:
> {"cmd": "click", "x": 512, "y": 400}
> {"cmd": "scroll", "x": 512, "y": 400, "delta": 200}
> {"cmd": "type", "text": "hello"}
> {"cmd": "navigate", "action": "tab"}
After every interaction, the harness checks for common issues and includes warnings in the response. You don't need to ask — problems surface automatically:
| Lint | Fires when |
|---|---|
no_effect |
Click produced zero changes |
contrast_regression |
Style change reduced text contrast below 3:1 |
content_lost |
More nodes removed than added |
focus_lost |
Focused element was removed or hidden |
empty_content |
New nodes have no text content |
buttons_no_handlers |
Buttons without event handlers |
scroll_no_effect |
Scroll didn't change position |
bulk_change |
>50 changes (info, not warning) |
Example: toggling a theme produces contrast warnings for elements that don't adapt:
> {"cmd": "click", "text": "Light"}
< {
"changes": {"total": 70, "styles": 70},
"lints": [
{"level": "warning", "lint": "contrast_regression",
"message": "Node 175 'Overview' now has low contrast 1.3:1 (color:#444 on bg:#313244)"}
]
}
Mark a snapshot, make changes, then diff:
> {"cmd": "snapshot_mark", "label": "before"}
> {"cmd": "click", "text": "Add Todo"}
> {"cmd": "diff", "from": "before"}
< {
"changes": [
{"NodeAdded": {"id": 500}},
{"TextChanged": {"id": 92, "before": "3 todos", "after": "4 todos"}},
{"StyleChanged": {"id": 88, "property": "background", "before": "#313244", "after": "#89b4fa"}}
]
}
Run any of the built-in analyzers on the live rendered app:
> {"cmd": "analyze", "type": "diagnostics"}
> {"cmd": "analyze", "type": "a11y"}
> {"cmd": "suggest_fixes"}
< {"suggestions": [
{"severity": "error", "message": "Button #88 has no click handler"},
{"severity": "warning", "message": "Text contrast 2.6:1 fails WCAG AA"}
]}
Get ASCII representations of the live rendered UI:
> {"cmd": "ascii", "mode": "tree"}
> {"cmd": "ascii", "mode": "structure"}
> {"cmd": "ascii", "mode": "css"}
Every response includes timing metadata:
< {"node_count": 245, "_queue_ms": 2, "_exec_ms": 15}
_queue_ms shows how long the command waited before the main thread processed it. If this is >100ms, the app is sluggish. If the main thread is completely blocked, the harness detects it and warns:
< {"warning": "app_frozen", "frozen_ms": 3200, "queued_commands": 2}
Frame-level profiling:
> {"cmd": "perf"}
< {"last_layout_ms": 2.3, "last_scene_build_ms": 0.8, "frame_count": 142}
# List running test sessions
rvst test list
# Kill a session
rvst test kill rvst-test-12345
Pipe a sequence of commands. The wait command pauses between steps without blocking the renderer:
echo '{"cmd":"wait","ms":2000}
{"cmd":"click","text":"Settings"}
{"cmd":"wait","ms":1000}
{"cmd":"click","text":"Light"}
{"cmd":"wait","ms":2000}
{"cmd":"click","text":"Dark"}
{"cmd":"wait","ms":1000}
{"cmd":"close"}' | rvst test launch dist/app.js
Combine with Unix tools:
# Find all buttons without handlers
rvst test launch dist/app.js <<< '{"cmd":"find","role":"button"}' | jq '.nodes[] | select(.has_handlers == false)'
# Check if click actually changed state
rvst test launch dist/app.js <<< '{"cmd":"click","text":"Submit"}' | jq '.changes.total'
| Category | Commands |
|---|---|
| State | snapshot, find, query, explain, computed_styles, accessibility_tree |
| Interaction | click, scroll, hover, type, navigate, focus |
| Visualization | ascii, screenshot, compare_pixels |
| Analysis | analyze, suggest_fixes, stacking_order, compare_layout |
| Assertions | assert_visible, assert_clickable, why_not_visible, hit_test, list_handlers |
| Diffing | snapshot_mark, diff |
| Performance | perf |
| Streaming | watch, watch_stop |
| Session | wait, close |
For programmatic access from Rust tests and tools:
use rvst_shell::HeadlessSession;
use rvst_shell::snapshot::SceneSnapshot;
let mut session = HeadlessSession::new("dist/app.js", 1024, 768);
let snap = session.snapshot();
// Query the scene graph
snap.assert_visible(node_id)?;
snap.assert_clickable(node_id)?;
snap.hit_test_stack(x, y);
snap.why_not_visible(node_id);
snap.accessibility_tree();
// Semantic node handles — stable across re-renders
let btn = snap.nodes.iter().find(|n| n.role == "button").unwrap();
println!("{} {} {:?}", btn.semantic_id, btn.role, btn.name);
// ASCII introspection
use rvst_shell::ascii;
println!("{}", ascii::tree(&snap));
println!("{}", ascii::tree_with_view(&snap, ascii::TreeView::Css));
println!("{}", ascii::structure(&snap, 160, 50));
RVST is early. Here's what doesn't work yet or works differently than you'd expect:
position: sticky, overflow: auto scrollbars, and many shorthand expansions are missing.<img> / <video> / <canvas>. Media elements are not implemented. Background images via CSS url() are not loaded.rvst dev rebuilds and restarts. There's no in-process HMR.Apache 2.0