A component library for gpui — the GPU-accelerated Rust UI framework that powers Zed — and Tailor, the visual interface builder for it.
guise gives gpui a batteries-included component layer: a themed palette,
sizing tokens, 130+ composable components, a reactive state layer with two-way
bindings, an animation system with keyframes and a scrubbable playhead, and the
full Lucide icon set embedded as the default icons — no
asset pipeline needed.
Tailor is the other half: a drag-and-drop builder that lays those same components out on a canvas and exports idiomatic Rust. Write the interface or draw it — either way you end up with the same components.
use guise::prelude::*;
Stack::new()
.gap(Size::Md)
.child(Title::new("Welcome").order(1))
.child(
Button::new("save", "Save changes")
.variant(Variant::Filled)
.color(ColorName::Blue)
.on_click(|_, window, cx| { /* ... */ }),
)Tailor is a drag-and-drop interface builder for gpui and guise, shaped like Interface Builder and Android Studio's layout editor. Lay out a screen from real components, wire the state and the actions, and export idiomatic Rust that has no dependency on Tailor left in it.
cargo run -p tailor-app # from a checkout (binary: tailordev)Or take the app: every release
attaches Tailor.dmg, signed and built from this repository, with the MCP
server beside the executable in the bundle.
The canvas is not a drawing of your interface — it is your interface. A
Button on it is a guise::Button, reading the same theme, laid out by the
same flexbox. There is no second rendering path to keep in step, so a component
cannot look right in the builder and wrong in the app.
| Part | What it does |
|---|---|
| The workbench | A searchable library of all 101 placeable components, the node outline, the artboard, a five-tab inspector (Attributes, Size, Style, Connections, Identity) and a Problems panel. Every panel resizes, folds away, and remembers where you left it. |
| Direct manipulation | Eight resize knobs around the selection, drag to move, snapping to the grid and to siblings' edges with guides drawn where it caught, a live size readout, and arrow-key nudging. |
| What comes out | A Render entity when the document holds state, a RenderOnce builder when it does not. State variables become Signal<T> fields, events become cx.listener / cx.subscribe, and every resolved colour is hoisted into a let at the top of render the way guise's conventions require. |
| A live window | A second window rendering the document for real, following every edit — with the guise DevTools inspector in it, and right-click-to-inspect the way a browser does it. |
| An MCP server | tailor-mcp drives the same document model, so an agent can place components, wire state and generate Rust with no window open. It saves after every change and the app watches the file, so a screen built by an agent appears on the canvas as it is built. |
| An editor jump | Both directions. Open in Editor (⌥⌘O) puts your cursor on the line a component generated; tailordev --reveal <file>:<line> goes the other way, and a Zed task binds it to a key. Zed, VS Code, Sublime, IntelliJ, Emacs and Neovim. |
Full documentation starts at docs/tailor.md, and
the tutorial builds a complete app end to end — every
code block in it is output Tailor actually produced.
Full docs live in docs/ (also rendered at
wess.github.io/guise):
- Tutorial — build a complete app step by step (web version)
- App walkthrough — a project tracker wired the way a real guise app fits together
- Motion tutorial — one animated panel, nine chapters (web version)
- Getting started · Theming · Component model
- Components: Buttons · Icons · Inputs · Dates & times · File handling · Typography · Layout · Panels · Feedback · Data · Charts · GPU View · Editor · Markdown editor · AI · Overlays · Navigation
- Systems: Flex layout · Macros · Motion & transitions · Drag & drop · Reactive state · Software update · Settings · DevTools · Window menu & chrome · Architecture · Size & performance
- Tailor: Overview · Tutorial · The canvas · Components & slots · State & actions · Generated code · MCP server · Zed & other editors
- Releasing · Changelog
crates/guise— the component library, published asguise-ui. The only crate here that reaches crates.io.crates/gallery— a live showcase of every component (cargo run -p gallery).crates/tailor/— Tailor, in sixpublish = falsecrates:model(the document, catalog and file format),codegen(document → Rust),store(project files, settings, export),render(document → live components),app(the workbench) andmcp(the MCP server).extensions/zed/— a Zed extension registeringtailor-mcpas a context server. Its own cargo workspace; it targetswasm32-wasip2.site/— the Bun generator that rendersdocs/into the website.
Nothing about Tailor is in guise-ui: cargo package -p guise-ui --list is the
proof, and it is why the library still depends on nothing but gpui and std.
Two other component libraries target gpui: gpui-component (shadcn-flavored, backs Longbridge Pro) and adabraka-ui. A quick orientation, as of July 2026:
| guise | gpui-component | adabraka-ui | |
|---|---|---|---|
| Design language | open-color palette + token scales | shadcn/ui | shadcn/ui |
| Components | 130+ | 60+ | ~140 |
| Visual builder | Tailor, in this repo | — | — |
| Reactive layer | Signal / Binding / lenses, reactive Form |
— (entities + subscriptions) | — |
| Icons | all 1,991 Lucide glyphs as an embedded font, zero setup | 99 Lucide SVGs via an assets crate | ~1,600 SVGs, copied into your app manually |
| Theming | open-color palette, JSON theme files, 6 presets, per-slot overrides | ~140 tokens, JSON themes with hot reload, 22 presets | 19 presets, theme behind a global Mutex |
| Code editor | 10-language highlighter + diagnostics API | tree-sitter (~35 languages) + LSP client | tree-sitter (22 languages) |
| Docking / panels | PaneGroup splits-with-tabs + layout persistence |
DockArea + floating Tiles + serialization |
resizable/split panels |
| Charts | 6 types with axes, legends, hover | 6 types incl. candlestick & Sankey on a plot framework | 11 types |
| Motion | easing curves, spring physics, exit animations | basic easing | large effects library |
| Drag & drop | typed payloads, sortable lists | panel docking | draggable + sortable |
| Date/time pickers | yes | yes (incl. range presets) | yes |
| Tests | 520+ incl. gpui entity harness | ~580 incl. render tests | minimal |
| gpui dependency | crates.io releases | crates.io releases; dev tracks zed main | a custom gpui fork |
| License | MIT | Apache-2.0 | MIT |
Reach for guise if you want chainable builders, SwiftUI-style two-way bindings, and icons that just work with zero asset setup. gpui-component is the bigger ecosystem — a production code editor with LSP, a full dock system, and a WASM story. adabraka-ui ships the largest effects/animation collection.
Install a theme once at startup; every component reads it from the gpui global:
guise::Theme::dark().init(cx); // or Theme::light()
guise::Theme::catppuccin().init(cx); // or nord / tokyonight / gruvbox / dracula / solarized_lightThe theme carries the full open-color
palette (14 colors × 10 shades), xs..xl scales for spacing, radius and font size, and scheme-aware semantic
colors (body, surface, text, dimmed, border, plus success /
warning / danger / info feedback accents). Themes also load from flat
JSON files — Theme::from_json(source) — with no serde dependency.
Write colors the CSS way — hex, rgb/rgba, hsl/hsla, or named — with the
color! macro (compile-time) or css(..) (runtime strings). Both produce a
gpui Hsla, usable in .bg(..)/.text_color(..), in any component .color(..),
and in the theme with_* overrides:
Button::new("go", "Go").color(color!(rgba(112, 72, 232, 1)))
Badge::new("New").color(color!("#e64980"))
Theme::dark()
.with_primary(color!("#7048e8"))
.with_body(color!(rgb(11, 11, 15)))
.with_text(color!("hsl(220, 15%, 92%)"))
.init(cx); // restyles the whole UIcolor! takes rgb(..)/rgba(..)/hsl(..)/hsla(..)/named tokens or a CSS
string (hex must be a string — #228be6 isn't a Rust token). Component
.color(..) accepts a palette ColorName or an explicit color (the
ColorValue type) and derives variant shades from a single custom color. See
Theming.
| Group | Components |
|---|---|
| Layout | Stack, Group, Center, SimpleGrid, ScrollArea, AppShell, Container, Space, Panel, SplitPanel, Breakpoint/Responsive |
| Surface | Paper, Card |
| Typography | Text, Title, Mark, Blockquote, Spoiler |
| Inputs | Button, TextInput, TextArea, NumberInput, PasswordInput, PinInput, Checkbox, Switch, Radio, RadioGroup, CheckboxGroup, Select, Combobox, Autocomplete, Slider, RangeSlider, Rating, ColorInput, TagsInput, Transfer, Field |
| Dates & files | Calendar, DatePicker, TimePicker, FileInput, Dropzone (with pure Date/Time models) |
| Editor | Editor (highlighting for Rust, SQL, JSON, TOML, Python, JS/TS, Go, C, Markdown; LSP-shaped diagnostics), MarkdownEditor (Obsidian-style live preview), Markdown (its read-only sibling) |
| AI | AIChatView, AIMessage, AIComposer, AIStreamingText, AIThinking, AIReasoning, AIToolCall, AICitation, AISources, AIModelPicker, AITokenMeter, AICost, AISettings — transport-agnostic; the host owns the request |
| Overlays | Modal, Drawer, Menu, MenuBar, ContextMenu, HoverCard, LoadingOverlay, ConfirmModal, Popover, Spotlight, Tooltip, Tour, OverlayHost (window-level modal stack + toasts) |
| Feedback | Alert, Loader, Progress, RingProgress, Notification, ToastStack |
| Data | Badge, Divider, Avatar, AvatarGroup, List, VirtualList, Table, TableView, DataView, TreeView, TabBar, Image, Timeline, Tabs, Accordion, Carousel |
| Workspace | PaneGroup — Zed-style splits-with-tabs, drag-to-split, and layout snapshots that persist |
| Charts | Sparkline, LineChart, AreaChart, BarChart, ScatterChart, PieChart — with optional axes, legends, and hover readouts |
| Scenes | GpuView, GpuScene, GpuTexture — retained native GPU surfaces for maps, simulations, and sprite-heavy status worlds |
| Navigation | Breadcrumbs, NavLink, NavigationMenu, Stepper, Pagination, StatusBar |
| Drag & drop | Draggable, DropTarget, SortableList — typed payloads |
| Motion | Transition, Collapse (true height animation), Presence (exit animations), Easing curves + Spring physics |
| Update | Updater (release check + in-place install), UpdatePrompt, UpdateNotice — a whole self-update feature |
| Settings | SettingsView, SettingsSection, SettingsRow — the settings screen: page list, groups, and label/control rows |
| DevTools | DevTools — an in-app Safari-style inspector: Elements, Network, Sources, Timelines, Storage, Layers, Logs, Audit |
| App chrome | About (with honest BuildKind dating), WindowControls, ResizeHandles |
| Polish | Icon (all of Lucide embedded), ActionIcon, ThemeIcon, CloseButton, CopyButton, Anchor, Code, Kbd, Chip, Indicator, Skeleton, SegmentedControl |
Inputs come in two flavors that match how each control behaves in gpui:
- Controlled (
Checkbox,Switch,Radio,Rating, and theRadioGroup/CheckboxGroupwrappers) areRenderOncebuilders — the parent view owns the value and passes a change handler viacx.listener(...). - Stateful (
TextInput,TextArea,NumberInput,PasswordInput,PinInput,Select,Combobox,Autocomplete,Slider,RangeSlider,ColorInput,TagsInput,DatePicker,TimePicker,FileInput,Transfer) are gpui entities that own their buffer / selection. Create withcx.new(...)and subscribe to their events.Fieldis the shared label/description/error chrome these compose.
Overlays paint above the page (a deferred layer): Modal and Drawer are
controlled backdrops, ConfirmModal a confirm/cancel dialog, Menu a
keyboard-navigable action list, MenuBar a themed in-window application menu,
ContextMenu opens at the pointer, Popover is the anchored-floating
primitive, HoverCard its hover-triggered sibling, Spotlight a command
palette, Tour a step-by-step onboarding walkthrough, and OverlayHost
owns a window-level modal stack and toast queue — open dialogs from any
handler, focus restores on close.
Data display scales from stateless builders (Avatar, List, Table,
Timeline) to virtualized, signal-bound entities: TableView<T> (typed rows,
sorting, selection, virtualized body), DataView<T> (list/grid over a
Signal<Vec<T>> with filter/sort projections and windowed rendering),
TreeView (expandable hierarchy, virtualizable), and VirtualList (100k rows
render as cheaply as 20). PaneGroup is the Zed-style splits-with-tabs
workspace, including drag-to-split and layout snapshots that persist across
sessions.
A Flutter-flavored layout kit on top of gpui's flexbox: Row, Column,
Container, Padding, Align, Center, Expanded/Flexible (real flex
weights), Spacer, SizedBox, Stack/Positioned, and Wrap, with
MainAxisAlignment / CrossAxisAlignment / EdgeInsets. It is not
glob-exported (names overlap with guise::layout); import it as
use guise::flex::*.
Terse builders, available from the prelude:
col![
row![avatar, name, Spacer::new(), actions], // guise::flex::Row
divider,
body,
]row!/col!/zstack!/wrap! build flex containers; vstack!/hstack!
build the themed layout::Stack/Group. Two more are declaration blocks
rather than containers: style! for inline styling and motion! for
animation, plus sequence! for motions on one clock. See
Macros.
A small React-flavored layer over gpui's reactivity:
let count = use_state(cx, 0i32); // Signal<i32>
provide(cx, count.clone()); // Context.Provider, keyed by type
watch(cx, &count); // re-render this view on change
count.update(cx, |n| *n += 1); // notifies every watcherBindings are SwiftUI-style two-way connections: controlled builders take
.bind(signal.binding()), stateful entities bind with
X::bind(&entity, &signal, cx) — the value flows both directions with no
hand-written change handlers. signal.lens(get, set) projects one struct
field; binding.map(from, into) converts types both ways.
Forms: every Form field is its own Signal<String>, so inputs bind
straight to fields; rules (including cross-field like equals_field) run on
submit and errored fields re-validate live as they're edited:
let form = Form::new(cx)
.field(cx, "email", "")
.rule("email", validators::email());
TextInput::bind(&email_input, &form.signal("email"), cx);
if let Some(values) = form.submit(cx) { /* … */ }See Reactive state.
An animation system in the shape of anime.js, split in
two. The description is pure — a Motion is keyframed tracks over a
duration, a Sequence puts motions on one clock, a Stagger turns an index
into a delay, and sample(t) maps a millisecond offset to a Frame with no
state and nothing to tick. The clock is a thin shell: .animate(id, clip)
plays a clip once when an element mounts, and Animator is an entity holding a
playhead you can play, pause, reverse, scrub and re-speed.
div().child(card).animate("card", motion! {
duration: 420;
ease: out back;
opacity: 0 => 1;
y: 12 => 0;
})Full easing matrix (in/out/in_out × ten curves, plus steps, a CSS
cubic-bezier solver and closed-form Spring physics), keyframes, stagger
with grid distance, Presence for exit animations on conditionals, and the
narrower Transition/Collapse wrappers when a fade or a real-height reveal
is all you need.
cargo run -p guise-ui --example motion # the API, in one window
cargo run -p guise-ui --example checklist # the motion tutorial's appSee Motion & transitions and the motion tutorial.
guise::ai is everything a model-facing app needs on screen — a transcript, a
prompt box, streaming feedback, reasoning blocks, tool calls, citations, and the
meters around a request:
let chat = cx.new(|cx| AIChatView::new(cx).max_width(760.0));
let composer = cx.new(|cx| AIComposer::new(cx).hint("Shift+Enter for a new line"));
chat.update(cx, |chat, cx| {
chat.push(AITurn::user(prompt), cx);
chat.begin_reply(cx);
});
chat.update(cx, |chat, cx| chat.push_delta(&token, cx)); // as tokens arriveNone of it opens a socket. A component library is the wrong place to keep
someone's API key, so the host owns the request and these own what the user sees
while it happens — which is also what makes them portable: the same
AIChatView drives a local model, a hosted API, or a replayed transcript,
because all it ever receives is text. Message bodies render through
markdown::Markdown, the read-only sibling of MarkdownEditor. See
AI.
guise::update is a whole self-update feature, not just its UI: it checks a
release feed, installs the new version in place, and restarts into it.
let updater = Updater::github("Acme", env!("CARGO_PKG_VERSION"), "acme/acme")
.codesign_requirement("anchor apple generic and certificate leaf[subject.OU] = TEAMID")
.before_restart(|cx| save_session(cx));
guise::update::start(updater.clone(), cx); // at launch, then hourly
guise::update::check_now(updater, cx); // "Check for Updates…"On macOS the release .dmg is mounted and rsynced onto the installed .app, so
the bundle keeps its path and inode and LaunchServices never sees a stale
registration; a Linux AppImage is renamed over itself. Everything else opens the
release page — and the prompt's button says so rather than promising an install it
can't perform. Updates are verified against your codesign requirement before
anything touches the installed app. UpdatePrompt and UpdateNotice are ordinary
entities, so they work embedded in a window you own as readily as in the ones
update::open creates. See Software update.
guise::devtools is Safari's Web Inspector, aimed at the gpui app it is running
inside — Elements, Network, Sources, Timelines, Storage, Layers, Logs and Audit.
DevToolsState::new().init(cx); // once at startup
let devtools = cx.new(DevTools::new); // then put it wherever you likeThe Elements tree is real introspection: every component tags its root with
.probe("Name"), which snapshots the element's StyleRefinement and brackets
prepaint, so the tree is the live component hierarchy and the Styles sidebar
shows the element's actual declarations, its real box model, and the source
location it was constructed at. Do the same in your own components and they
appear alongside the library's.
It reads as an indented tree with YAML-flow props, not as markup — these are
components built from builder calls, not tags, so there is no
attributes-versus-children distinction to draw and <Button … /> would promise
a model gpui does not have. The recorder is scoped to one window, so put the
inspector in the window whose components you want to see.
Logs, Network, Storage and Timelines are reported by the host — nothing in
guise opens a socket — and Audit runs rules over the recorded tree (WCAG text
contrast, hit-target size, collapsed containers, children escaping their
parent). A probe costs one boolean check per element per frame while the
inspector is closed, and an app that never constructs one links none of it.
cargo run -p guise-ui --example devtoolsSee DevTools.
Colored components share one variant system: Filled, Light,
Outline, Subtle, Default, Transparent, White.
guise builds against crates.io gpui 0.2.2 — no git pins, no patch
sections:
[dependencies]
guise-ui = "1.5"
gpui = "0.2.2"The crate is named
guise-ui(theguisename was taken on crates.io), but its library is namedguise— so you still writeuse guise::prelude::*;.
Pinning via git works too:
guise-ui = { git = "https://github.com/wess/guise", tag = "v1.5.3" }Requires Rust stable.
cargo run -p gallery # launch the component gallery
cargo run -p tailor-app # launch Tailor, the interface builder
cargo test -p guise-ui # the library's tests (unit + gpui entity harness)
cargo test -p tailor-model -p tailor-codegen -p tailor-store # Tailor's pure half
cd site && bun run build.ts # render docs/ into site/distCutting a release — the version, the lockfile, the tag, the signed Tailor.dmg
and the crates.io publish — is written down in
releasing.
MIT — see LICENSE.