A declarative, reactive GUI framework for Rust, built on top of
gpui (the GPU-accelerated UI toolkit
behind the Zed editor).
vgui brings a familiar web-style authoring experience to native Rust
desktop apps and web (WASM) applications:
- JSX-like views via the
view!macro — elements, attributes, children, fragments and component invocation, all in ergonomic markup. - CSS-in-Rust via the
css!macro — write real CSS declarations (color: #fff; padding: 8px;) that compile down togpuistyle refinements. - Tailwind-style classes via the
tw!macro —class="p-2 rounded hover:bg-[#000088]"works out of the box, withhover:,focus:andactive:variants. - Fine-grained reactivity inspired by SolidJS:
create_signal,create_memoandcreate_effecttrack dependencies automatically and re-render only what changes. - Control-flow components
<Show>and<For>for conditional and list rendering with optional fallbacks. - ARIA semantic attributes —
roleandaria:nameon all elements for accessibility. - Context & Provider — SolidJS-style dependency injection via
Context<T>and<Provider>. - NodeRef — imperative handles for focus, scroll, and bounds queries.
- Focus management — focus trap, focus restore, and roving tabindex for keyboard navigation.
- Overlays —
portal,dialog, andfloatingfor modals and floating elements on a separate layer. - Component variants — the
variants!macro declares base + dimension styles that compose into typed,Copyvariant structs. - Animations & transitions —
tw!animate-*andtransition-*utilities with easing and keyframe support. - Responsive breakpoints —
sm:,md:,lg:,xl:prefixes for viewport-conditional styling. - Dynamic class composition — the
twc!macro composes conditional Tailwind classes at runtime. - CSS variables & theming —
theme!macro,var(--name)incss!, andset_theme()for reactive light/dark switching. - SPA router — signal-driven router with
:parampattern matching and wildcard routes. - Dual-target — every example compiles and runs natively (Linux) and on the web (WASM) with a single codebase.
vgui is early-stage, experimental software. The API is not yet stable and
breaking changes should be expected between releases. It is, however, fun to
build with — see the examples.
- Workspace layout
- Prerequisites
- System libraries
- Building
- Examples
- Usage
- Reactivity
- Styling
- Control flow
- Tables
- Input elements
- ARIA & Accessibility
- Context & Provider
- Refs & NodeRef
- Focus Management
- Overlays (Portal/Dialog/Floating)
- Component Variants
- Animations & Transitions
- Responsive Breakpoints
- Dynamic Classes
- CSS Variables & Theming
- Router
- Web / WASM
- Documentation
This repository is a Cargo workspace:
| Crate | Kind | Description |
|---|---|---|
vgui |
lib | The main crate: reactivity, root mounting, styling traits, widgets. |
vgui-view |
proc-macro | The view! macro. |
vgui-css |
proc-macro | The css! macro. |
vgui-tailwind |
proc-macro | The tw! macro and the Tailwind class registry. |
vgui-tailwind-core |
lib | Shared class-parse/tables for tw! and tw_dynamic (no gpui dep). |
Sixteen example binaries live under examples/ — see
Examples below and the book's Examples section
for the full list.
- A recent nightly Rust toolchain (pinned via
rust-toolchain.toml). pkg-config.- A C/C++ build toolchain:
build-essential,cmake. libclangforbindgen(used bygpuiand its transitive deps).
vgui does not depend on system libraries directly, but gpui does — it
talks to the native window system (Wayland and X11 on Linux, Cocoa/Metal on
macOS, Win32/DirectX on Windows). The following development packages are
required to build gpui on a Debian/Ubuntu Linux host:
sudo apt-get install -y \
build-essential \
cmake \
pkg-config \
libclang-dev \
libssl-dev \
libzstd-dev \
libfontconfig1-dev \
libfreetype6-dev \
libglib2.0-dev \
libgtk-3-dev \
libasound2-dev \
libdbus-1-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev \
libx11-dev \
libxext-dev \
libxrandr-dev \
libxinerama-dev \
libxcursor-dev \
libxi-dev \
libwayland-dev \
libgl-dev \
libegl-devFedora / RHEL
sudo dnf install -y \
clang-devel openssl-devel libzstd-devel fontconfig-devel freetype-devel \
glib2-devel gtk3-devel alsa-lib-devel dbus-devel \
libxkbcommon-devel libxkbcommon-x11-devel \
libX11-devel libXext-devel libXrandr-devel libXinerama-devel \
libXcursor-devel libXi-devel wayland-devel \
mesa-libGL-devel mesa-libEGL-devel \
cmake pkg-configArch Linux
sudo pacman -S --needed \
base-devel clang cmake pkgconf \
openssl zstd fontconfig freetype2 glib2 gtk3 alsa-lib dbus \
libxkbcommon libxkbcommon-x11 \
libx11 libxext libxrandr libxinerama libxcursor libxi wayland \
mesamacOS
No extra system libraries are required beyond Xcode Command Line Tools:
xcode-select --installgpui uses Metal/Cocoa natively on macOS.
Windows
Build with the MSVC toolchain (rustup default stable-x86_64-pc-windows-msvc)
and the Visual Studio C++ Build Tools.
The Windows SDK provides the rest.
git clone https://github.com/vgerbot-libraries/vgui.git
cd vgui
cargo buildThe first build compiles gpui and its graphics backends, so expect a longer
initial compile. Subsequent incremental builds are fast.
To check the WASM target:
cargo +nightly check --target wasm32-unknown-unknownSee Web / WASM below for building and serving examples in the browser.
Sixteen end-to-end examples live under examples/:
| Example | Command | Description |
|---|---|---|
| Counter | cargo run -p vgui-counter |
Signals, create_memo, <Show>, twc! class composition. |
| Todo List | cargo run -p vgui-todolist |
<For> with fallback, css! styling, filtering, CRUD. |
| Styling Showcase | cargo run -p vgui-styling |
css! macro, Tailwind classes, pseudo-states, twc!, responsive breakpoints. |
| Theming | cargo run -p vgui-theming |
CSS variables, theme! macro, light/dark switching. |
| Component Variants | cargo run -p vgui-variants |
variants! macro, typed variant structs, ApplyStyle. |
| Inputs | cargo run -p vgui-inputs |
All <input> types, <select> with groups/multiple/custom rendering. |
| HTML Elements | cargo run -p vgui-elements |
HTML tag coverage, tables, progress, details, dialog. |
| Forms | cargo run -p vgui-forms |
<form> submission, reset, field grouping, enter-to-submit. |
| Context & Provider | cargo run -p vgui-context |
Context API, <Provider>, use_context, multi-module. |
| Refs & NodeRef | cargo run -p vgui-refs |
NodeRef imperative handles (focus, scroll, bounds). |
| Focus Management | cargo run -p vgui-focus |
Focus trap, restore, roving tabindex, on:resize. |
| Overlays | cargo run -p vgui-overlays |
portal(), dialog(), floating() overlay patterns. |
| Animation | cargo run -p vgui-animation |
Animations, transitions, keyframes, custom animate={...}. |
| Canvas | cargo run -p vgui-canvas |
<canvas>, Context2D API, shapes, paths, text, transforms. |
| Router | cargo run -p vgui-router |
SPA router with param matching, navigation, wildcard routes. |
| Dashboard | cargo run -p vgui-dashboard |
Capstone: router + theming + context + forms + overlays. |
Add vgui (and gpui) to your Cargo.toml. Both are used as git
dependencies — neither crate is published to crates.io:
[dependencies]
vgui = { git = "https://github.com/vgerbot-libraries/vgui" }
gpui = { git = "https://github.com/zed-industries/zed" }
gpui-platform = { git = "https://github.com/zed-industries/zed", package = "gpui_platform" }vgui can also be used as a path dependency if you have a local checkout.
Then author a window with view!:
use gpui::{px, size, App, Application, Bounds, WindowBounds, WindowOptions};
use vgui::prelude::*;
fn app() -> impl gpui::IntoElement {
let (count, set_count) = create_signal(0i32);
view! {
<div class="flex flex-col gap-3 p-4 w-[500px] h-[500px] justify-center items-center text-white">
<span>{format!("count = {}", count.get())}</span>
<button
class="p-2 bg-[#0000ff] hover:bg-[#000088] text-white rounded"
on:click={click(move |cx| set_count.update(cx, |n| *n += 1))}
>
{"Increment"}
</button>
</div>
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| vgui::mount(window, cx, app),
)
.unwrap();
});
}The vgui::prelude::* import brings in view!, css!, tw!, twc!,
variants!, theme!, the reactive primitives, click, mount, context API,
NodeRef, input widget constructors, styling types, and overlay helpers.
vgui uses a SolidJS-style reactivity model on top of gpui entities.
| Primitive | Purpose |
|---|---|
create_signal(v) |
Returns (ReadSignal, WriteSignal) for a piece of state. |
ReadSignal::get() |
Reads the value and registers the current scope as a dep. |
ReadSignal::get_with(cx) |
Reads the value without tracking a dependency. |
WriteSignal::update(cx, f) |
Mutates the value and notifies dependents. |
create_memo(f) |
A derived, cached value that recomputes when deps change. |
create_effect(f) |
Runs a side effect whenever its deps change. |
Signals are read inside view! interpolations ({count.get()}) and inside
create_memo / create_effect closures; vgui tracks which signals each
scope reads and re-runs only that scope when they change.
Two complementary macros are available.
view! {
<div style={css! {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
background: rgb(30, 30, 30);
color: #fff;
}}>
<span>{"Hello"}</span>
</div>
}Supported value forms include px, rem, %, auto, hex colors (#fff,
#ff0000, #0000ff80), rgb(...)/rgba(...), and named colors
(black, white, red, ...). Pseudo-state refinements go on the element
itself via the hover, active and focus attributes:
view! {
<button
style={css! { padding: 8px 16px; background: #dc2626; border-radius: 4px; }}
hover={css! { background: #b91c1c; }}
on:click={click(|_cx| {})}
>
{"Delete"}
</button>
}view! {
<div class="flex flex-col gap-3 p-4 bg-[#505050] w-[500px] h-[500px] justify-center items-center text-white">
<button class="p-2 bg-[#0000ff] hover:bg-[#000088] rounded">{"Click"}</button>
</div>
}The class="..." attribute is expanded through tw! and supports the
common spacing, sizing, color, flex, layout and typography utilities, plus
hover:, focus: and active: variants. Arbitrary values are written as
bg-[#0000ff], w-[500px], etc.
The twc! macro composes conditional Tailwind classes at runtime — a base
string plus Option<&str> arguments that are included only when Some:
<button class={twc!(
"p-2 rounded text-white",
(delta > 0).then_some("bg-blue-500"),
(delta < 0).then_some("bg-red-500")
)}>See Dynamic Classes below and the book.
sm:, md:, lg:, xl: prefixes apply styles only when the viewport width
meets the threshold. See the
book.
view! {
<Show when={count.get() > 0}>
<span>{"positive"}</span>
</Show>
<Show when={count.get() & 1 == 1} fallback={view! { <span>{"even"}</span> }}>
<span>{"odd"}</span>
</Show>
}view! {
<For each={todos.get()} fallback={view! { <div>{"No todos."}</div> }}>
{move |todo: Todo, _i: usize| todo_item(todo, set_todos.clone())}
</For>
}The child of <For> must be a closure move |item, index| -> impl IntoElement.
Any uppercase-tag element is treated as a component call. With no attributes
and a single child it expands to Component(child); with attributes it
expands to a struct initializer Component { field: value, ... }. Event
attributes (on:click) map to on_click, etc.
fn greeting(name: &'static str) -> impl gpui::IntoElement {
view! { <span>{format!("Hello, {name}!")}</span> }
}
view! {
<div>
<Greeting name={"world"} />
</div>Table tags are supported via flex layout (gpui has no native table layout).
<table>, <thead>, <tbody>, and <tfoot> stack their children vertically
(flex_col); <tr> is a horizontal flex row, full width; <td> and <th>
are flex_1 cells that share row width equally. <th> defaults to bold +
centered text. colspan is mapped to flex_grow, so a cell with
colspan={2} grows 2× relative to colspan=1 cells. rowspan, <colgroup>,
and <col> are accepted (they compile) but have no visual effect — there is
no content-based column sizing in a flex layout. For specific column widths,
apply class="w-[200px]" or a style width on individual cells.
view! {
<table class="w-full">
<thead>
<tr class="bg-[#333]">
<th class="p-2 text-white">{"Name"}</th>
<th class="p-2 text-white">{"Age"}</th>
<th class="p-2 text-white">{"City"}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="p-2">{"Alice"}</td>
<td class="p-2">{"30"}</td>
<td class="p-2">{"Beijing"}</td>
</tr>
<tr>
<td class="p-2" colspan={2u32}>{"Bob (spanned 2 cols)"}</td>
<td class="p-2">{"Shanghai"}</td>
</tr>
</tbody>
</table>
}<input> is a void element (self-closing — both <input type="text"> and
<input type="text" /> are accepted). The type attribute selects the
widget kind; if omitted, type="text" is assumed.
text, password, search, email, url, tel, number, date,
datetime-local, time, month, week, color all render a text field
with full cursor, selection, keyboard editing, clipboard (Ctrl+A/C/V/X), and
IME (CJK composition) support. Date, time, and color types have picker
popups.
view! {
<input
type="text"
placeholder="Name"
on:input={move |v: &str, cx: &mut App| set_name.set(cx, v.to_string())}
/>
}Supported attributes: value, placeholder, disabled, readonly, min,
max, step (for number), on:input (fires on every keystroke), on:change
(fires on Enter/blur), plus style, class, hover, active, focus, id.
view! {
<input type="checkbox" checked={done.get()} on:change={move |v: bool, cx: &mut App| set_done.set(cx, v)} />
<input type="radio" checked={sel.get() == 0} on:change={move |_v: bool, cx: &mut App| set_sel.set(cx, 0)} />
}view! {
<input type="range" min={0.0f64} max={100.0f64} step={1.0f64} value={vol.get()}
on:change={move |v: f64, cx: &mut App| set_vol.set(cx, v)} />
}view! {
<input type="file" value="Browse..." multiple={true}
on:change={move |paths: Vec<std::path::PathBuf>, _cx: &mut App| {
eprintln!("selected: {:?}", paths);
}} />
}These render as clickable buttons (like <button>). The value attribute
becomes the button label. on:click wires the handler. Inside a <form>,
submit and reset buttons auto-invoke the form's on:submit / on:reset
handler.
Hidden
<input type="hidden"> renders nothing (gpui::Empty).
<select> supports options (a Vec<(String, String)> of value/label
pairs), groups (grouped options with <optgroup>-style labels), multiple
(multi-select mode), value, disabled, and on:change. See the
book for details.
<datalist> provides autocomplete suggestions for text inputs. Register an
id and options={Vec<String>}, then reference it from an <input> via
list=<id>.
<form> wraps children in a form context. on:submit and on:reset take
FnMut(&mut App) closures. Child submit/reset buttons auto-invoke the form
handler. Pressing Enter in a single-line text input triggers on:submit.
All elements support role and aria:name attributes for ARIA roles,
labels, and states. See the
book.
Context<T> is a zero-sized typed marker; <Provider context={..} value={..}> pushes a value for descendants to consume via use_context or
use_context_or. See Context & Provider.
ref={node_ref} binds a NodeRef handle to an element for imperative
operations: focus(), scroll_to_bottom(), bounds(). See
Refs & NodeRef.
vgui provides focus trap (for modals), focus restore (return focus when an overlay closes), and roving tabindex (for radio groups with arrow-key navigation). See the Focus Management example.
portal(content, priority) renders content on a floating layer at a given
priority. dialog(open, on_close, content) wraps content in a modal dialog
with click-outside and Escape dismissal. floating(position, content)
renders a positioned floating element. See the
book.
The variants! macro declares a base style plus dimensions (e.g., variant
for color, size for padding). It generates typed enum variants and a
Copy struct that implements ApplyStyle. See the
Variants example.
tw! supports animate-* and transition-* utilities with easing functions
and keyframe definitions. See Animations & Transitions.
Four prefixes — sm: (≥640px), md: (≥768px), lg: (≥1024px), xl:
(≥1280px) — apply styles only when the viewport width meets the threshold.
See the book.
The twc! macro composes conditional Tailwind classes at runtime. TwClass
provides a builder API (add(), add_if()). tw_dynamic(classes: &str)
interprets class strings at runtime. See the
book.
The theme! macro builds a Theme from --name: value declarations.
var(--name) in css! resolves against the thread-local theme store at
runtime. set_theme() installs a theme reactively — toggling a signal
re-runs render and re-resolves all var() references. See the
Theming example.
create_router(initial) creates a signal-driven Router. navigate(cx, path) updates the path; match_pattern supports :param segments and *
wildcards; render(cx, routes, fallback) dispatches the first matching
route. See Router.
vgui is dual-target: every example compiles for both native and
wasm32-unknown-unknown. On WASM, use gpui_platform::single_threaded_web()
(not application()), and call vgui::intercept_keyboard_events() in every
start() function to prevent the browser from stealing keyboard focus.
Build WASM assets with:
scripts/build_wasm.sh <name>See the writing-examples rule for the dual-target pattern.
The full documentation is an mdBook built with:
scripts/build_docs.shThe output is written to book/book/. To build and serve:
scripts/build_docs.sh --serveThe site is then available at http://127.0.0.1:8080.
Licensed under the MIT License.