Skip to content

Repository files navigation

RVST

A native desktop engine for Svelte

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


What is RVST?

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:

  • 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)

Quick Start

Install

npm install -g @rvst/cli

This installs the rvst command globally. It downloads the correct Rust binary for your platform automatically.

Create a new app

rvst create my-app
cd my-app
npm install

Write your first component

<!-- src/App.svelte -->
<script>let count =$state(0);</script>
<divclass="app">
<h1>Hello from RVST</h1>
<buttononclick={() =>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: 8px16px;border-radius: 6px;background: #3b82f6;color: white;border: none;font-size: 14px;cursor: pointer; }</style>

Entry point

// src/entry.jsimport{mount}from'svelte';importAppfrom'./App.svelte';exportfunctionrvst_mount(target){returnmount(App,{ target });}exportdefaultrvst_mount;

Vite config

// vite.config.jsimport{defineConfig}from'vite';import{svelte}from'@sveltejs/vite-plugin-svelte';import{rvstPlugin}from'@rvst/vite-plugin';exportdefaultdefineConfig({plugins: [rvstPlugin(),svelte()],build: {outDir: 'dist',target: 'esnext',lib: {entry: 'src/entry.js',formats: ['es'],fileName: 'app',},},});

Build and run

rvst build
rvst run

Or with watch mode for development:

rvst dev

How It Works

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.

Native APIs

RVST exposes native platform capabilities to Svelte via globalThis.__rvst:

Window Management

<script>constrvst=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>

File System

<script>constfs=globalThis.__rvst?.fs;asyncfunctionloadConfig() {consttext=fs.readText('/path/to/config.json');returnJSON.parse(text); }functionsaveConfig(data) {fs.writeText('/path/to/config.json', JSON.stringify(data, null, 2)); }</script>

Available APIs

APIDescription
__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

Configuration

CLI

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

Fonts

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; }

CSS Support

RVST's CSS engine (powered by lightningcss) supports:

  • Full cascade with specificity and source order
  • !important override
  • CSS custom properties with ancestor inheritance (var(--theme-bg))
  • @media queries (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() backgrounds
  • transform: translate, rotate, scale, skew
  • text-decoration: underline, line-through, overline
  • Flexbox and CSS Grid (via Taffy)
  • border-radius, box-shadow (multiple), opacity

Templates

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 primitives

Architecture

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 Support

PlatformStatusNotes
macOS (Apple Silicon)StablePrimary development platform. Metal backend.
macOS (Intel)StableMetal backend.
Linux (X11/Wayland)SupportedVulkan backend. Install vulkan-loader and GPU drivers.
Windows 10/11SupportedDX12 backend (Vulkan fallback).
Headless/CISupportedSoftware rendering via LLVMpipe/SwiftShader.

Linux Setup

# Ubuntu/Debian
sudo apt install build-essential pkg-config libvulkan-dev libwayland-dev
# Fedora
sudo dnf install vulkan-loader-devel wayland-devel

Windows Setup

No additional dependencies. wgpu uses DX12 natively. Ensure GPU drivers are up to date.

Headless Mode

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")'

ASCII Scene Introspection

RVST can visualize UI state as text — useful for AI agents, debugging, and CI validation. All examples below are from the same dashboard app:

RVST rendering a full dashboard app with sidebar, stats, and activity feed
Dashboard app — traffic lights, sidebar nav with icons, stat cards, activity feed

Visualization Modes

Structure — box-drawing layout map showing element boundaries and nesting:

rvst --ascii=structure dist/app.js

ASCII structure map of the dashboard

Render — pixel-sampled ASCII art of the actual GPU output:

rvst --ascii=render dist/app.js

ASCII pixel render of the dashboard

Overlay — pixel background with semantic labels overlaid:

rvst --ascii=overlay dist/app.js

ASCII overlay combining pixels and labels

Validate — cross-checks tree against pixels, marks mismatches with !:

rvst --ascii=validate dist/app.js

ASCII validation showing tree vs pixel mismatches

Tree Views

Semantic tree (default) — compact, agent-friendly:

rvst --ascii dist/app.js

Semantic tree view of the dashboard

Tree with CSS — classes and key computed properties:

rvst --ascii=tree:css dist/app.js

Tree view with CSS classes and properties

Tree with layout — computed rects (position + size):

rvst --ascii=tree:layout dist/app.js

Tree view with layout rects

Full tree — role + classes + rects combined:

rvst --ascii=tree:full dist/app.js

Full tree view with roles, CSS, and layout

Filtering

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

Scene Analysis

RVST includes built-in analyzers that inspect your UI for layout issues, accessibility gaps, and contrast problems. Each produces a colored terminal report.

Diagnostics

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

Diagnostics report showing zero-size and no-handler warnings

Layout

Quantifies your UI's spatial characteristics — node count, nesting depth, viewport utilization, whitespace ratio, and flex direction distribution:

rvst analyze layout dist/app.js

Layout analysis with depth histogram and utilization stats

In this dashboard: 245 nodes, max depth 7, only 23.5% viewport utilization — most content is in the center, leaving the bottom half empty.

Accessibility

Audits interactive elements for semantic completeness — buttons without accessible names, interactive elements without handlers, role distribution:

rvst analyze a11y dist/app.js

Accessibility audit showing unlabeled buttons and missing handlers

Contrast (WCAG 2.1)

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

WCAG contrast analysis with per-text-node ratios

Shows actual foreground/background colors sampled from the GPU render — not CSS values, but what the user sees.

Density Heatmap

Visualizes where UI elements cluster in the viewport as a truecolor terminal heatmap:

rvst analyze heatmap dist/app.js

Density heatmap showing element clustering

Cold (blue) = empty space, hot (red) = many overlapping elements. In this dashboard, the sidebar and stat cards are the densest regions.

Run Everything

rvst analyze all dist/app.js

RenderQuery Test Harness

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

RenderQuery test harness — querying state, clicking buttons, detecting contrast regressions

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.

Querying State

# 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)"]}

Interacting with the UI

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"}

Automatic Lints

After every interaction, the harness checks for common issues and includes warnings in the response. You don't need to ask — problems surface automatically:

LintFires when
no_effectClick produced zero changes
contrast_regressionStyle change reduced text contrast below 3:1
content_lostMore nodes removed than added
focus_lostFocused element was removed or hidden
empty_contentNew nodes have no text content
buttons_no_handlersButtons without event handlers
scroll_no_effectScroll 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)"}
]
}

Diffing State Changes

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"}}
]
}

Analysis

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"}
]}

Visualization

Get ASCII representations of the live rendered UI:

> {"cmd": "ascii", "mode": "tree"}
> {"cmd": "ascii", "mode": "structure"}
> {"cmd": "ascii", "mode": "css"}

Performance

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}

Session Management

# List running test sessions
rvst test list
# Kill a session
rvst testkill rvst-test-12345

Scripting

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'

Full Command Reference

CategoryCommands
Statesnapshot, find, query, explain, computed_styles, accessibility_tree
Interactionclick, scroll, hover, type, navigate, focus
Visualizationascii, screenshot, compare_pixels
Analysisanalyze, suggest_fixes, stacking_order, compare_layout
Assertionsassert_visible, assert_clickable, why_not_visible, hit_test, list_handlers
Diffingsnapshot_mark, diff
Performanceperf
Streamingwatch, watch_stop
Sessionwait, close

RenderQuery API (Rust)

For programmatic access from Rust tests and tools:

use rvst_shell::HeadlessSession;use rvst_shell::snapshot::SceneSnapshot;letmut 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-renderslet btn = snap.nodes.iter().find(|n| n.role == "button").unwrap();println!("{} {} {:?}", btn.semantic_id, btn.role, btn.name);// ASCII introspectionuse rvst_shell::ascii;println!("{}", ascii::tree(&snap));println!("{}", ascii::tree_with_view(&snap, ascii::TreeView::Css));println!("{}", ascii::structure(&snap,160,50));

Limitations

RVST is early. Here's what doesn't work yet or works differently than you'd expect:

  • ~60 CSS properties supported. Common layout, color, typography, and transform properties work. Animations, transitions, position: sticky, overflow: auto scrollbars, and many shorthand expansions are missing.
  • No text selection or clipboard. Text renders but can't be selected or copied.
  • No IME / international input. Latin keyboard input works. CJK composition, emoji pickers, and dead keys are not wired up.
  • No scrollbar rendering. Content scrolls via mouse wheel / trackpad, but no visible scrollbar UI.
  • No SVG. Inline SVG elements are ignored. Use icon fonts or images instead.
  • No <img> / <video> / <canvas>. Media elements are not implemented. Background images via CSS url() are not loaded.
  • Single-window only. Multi-window, dialogs, and popups are not supported.
  • No hot module reload.rvst dev rebuilds and restarts. There's no in-process HMR.
  • macOS is the primary test target. Linux and Windows builds compile and run but receive less testing.
  • Text measurement can be slightly off. Parley migration in progress for pixel-accurate text width.

License

Apache 2.0

About

Run Svelte in Rust - no webview!

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages