Svelte is the language. Rust is the kernel. Your app owns every pixel.
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 Deno 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:
- Svelte 5 with runes, reactivity, scoped styles, component composition
- Tailwind v4 with full utility class support and design tokens
- CSS custom properties, @media queries, !important, :not(), :nth-child
- Custom window chrome (traffic lights, Windows controls, or your own)
- Native file system access from Svelte components
- GPU-accelerated rendering with Vello
- Icon fonts (Phosphor, Material Symbols, etc.)
- Headless rendering and scene graph queries (RenderQuery)
npm install -g @rvst/cliThis 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 runOr with watch mode for development:
rvst devSvelte Component (.svelte)
|
v
Vite + vite-plugin-rvst
(compiles to JS bundle)
|
v
Deno 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:
- Full cascade with specificity and source order
!importantoverride- CSS custom properties with ancestor inheritance (
var(--theme-bg)) @mediaqueries (min-width, max-width, min-height, max-height)@layer(Tailwind v4)- Selectors: class, ID, tag, attribute,
:not(),:first-child,:last-child,:nth-child - Pseudo-classes:
:hover,:focus,:active - Combinators: descendant, child (
>), adjacent (+), sibling (~) calc()with rem, em, px, vw, vh, %linear-gradient()backgroundstransform: translate, rotate, scale, skewtext-decoration: underline, line-through, overline- Flexbox and CSS Grid (via Taffy)
border-radius,box-shadow(multiple),opacity
rvst 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 primitivespackages/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-deno/ Deno 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 → Deno 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-develNo 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.jsRender — pixel-sampled ASCII art of the actual GPU output:
rvst --ascii=render dist/app.jsOverlay — pixel background with semantic labels overlaid:
rvst --ascii=overlay dist/app.jsValidate — cross-checks tree against pixels, marks mismatches with !:
rvst --ascii=validate dist/app.jsSemantic tree (default) — compact, agent-friendly:
rvst --ascii dist/app.jsTree with CSS — classes and key computed properties:
rvst --ascii=tree:css dist/app.jsTree with layout — computed rects (position + size):
rvst --ascii=tree:layout dist/app.jsFull tree — role + classes + rects combined:
rvst --ascii=tree:full dist/app.jsFilter 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.jsRVST 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.jsQuantifies your UI's spatial characteristics — node count, nesting depth, viewport utilization, whitespace ratio, and flex direction distribution:
rvst analyze layout dist/app.jsIn 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.jsSamples 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.jsShows 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.jsCold (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.jsRVST 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.jsThe 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-12345Pipe 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.jsCombine 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));Apache 2.0














