Twill Docs
Twill is a type-safe styling layer for native Rust GUI applications.
Use this guide for token selection, style composition, and backend mapping for egui, iced, and slint.
Getting Started
Start here:
Installation
Minimum setup
Choose the smallest crate that fits your use case.
Backend-agnostic core only:
[dependencies]
twill-core = "0.3"
Facade crate with the same core API:
[dependencies]
twill = "0.3"
Direct adapter crate without the facade:
[dependencies]
twill-core = "0.3"
twill-egui = "0.3"
# or twill-iced / twill-slint
MSRV: Rust 1.93.
Enable GUI backends
Enable only the backends you use:
[dependencies]
twill = { version = "0.3", features = ["egui"] }
# or
twill = { version = "0.3", features = ["iced"] }
# or
twill = { version = "0.3", features = ["slint"] }
You can combine features:
[dependencies]
twill = { version = "0.3", features = ["egui", "iced", "slint"] }
Feature notes
twill-corestays backend-agnostic and does not require a GUI runtime.twill-egui,twill-iced, andtwill-slinteach depend only ontwill-coreplus their own runtime crate.- Base
twillsimply re-exportstwill-core; it does not require a GUI runtime until you enable a backend feature. eguienables egui conversion helpers only.icedenables the Iced adapter and the Linux windowing/runtime feature set used by this crate configuration.slintenables Slint conversion helpers only.
Verify installation
Run:
cargo check
If you enabled backend features, you can verify the crate builds with:
cargo check --features egui
cargo check --features iced
cargo check --features slint
If you are working in the Twill repository itself, validate the full workspace with:
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo check --workspace --all-features --examples
Quick Start
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
let style = Style::card()
.merged(Style::interactive())
.padding(Padding::symmetric(Spacing::S2, Spacing::S4))
.hover(|style| style.opacity(0.9))
.at_md(|style| style.padding(Padding::all(Spacing::S6)));
}
Recommended imports:
twill::prelude::core::*for normal application code.twill::prelude::theme::*when you need semantic tokens and theme resolvers.twill::prelude::arbitrary::*when you need typed arbitrary/custom-property values.twill::prelude::traits::*forMerge,Responsive, and other integration traits.
#![allow(unused)]
fn main() {
use twill::prelude::{arbitrary::*, core::*};
let style = Style::new()
.bg_arbitrary(ColorValueToken::from_rgb8(15, 23, 42))
.text_color_arbitrary(ColorValueToken::from_rgb8(248, 250, 252))
.px_var(PaddingVar::new("--panel-pad-x"))
.min_w_var(WidthVar::new("--panel-min-w"))
.tracking_em(0.035)
.transition_custom("filter, transform");
}
Migrating From 0.2.x
This branch documents the 0.3.x API only.
If you are coming from 0.2.x, the important change is conceptual:
Twill is now a style-composition crate, not a component crate and not a CSS serialization layer.
What changed
Styleis the center of the public API.- Backend adapters stay feature-gated and convert Twill values into
egui,iced, andslinttypes. - Legacy component demos and CSS-oriented APIs belong to the
0.2.xline.
Recommended 0.3.x path
Start with:
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
}
Use the full prelude only when you need advanced arbitrary/custom-property wrappers:
#![allow(unused)]
fn main() {
use twill::prelude::{arbitrary::*, core::*};
}
Practical API updates
- Prefer
Style::background_color(...)when discoverability matters;bg(...)still exists. - Prefer
data_attr(DataState::..., ...)andaria_attr(AriaAttr::..., ...)over raw strings. - Prefer
Style::at_breakpoint(...)in normal application code. - Use
at_sm/at_md/at_lg/at_xl/at_2xlif the short breakpoint builders feel too terse. - Prefer
Style::surface(),Style::card(), andStyle::interactive()as reusable starting points. - Prefer
merged(...)ormerge_in_place(...)for explicit style composition.
What did not change
- The short DSL remains available.
- Raw escape hatches like
data_state("...")andaria_state("...")still exist. - Backend support is still opt-in through Cargo features.
Validation checklist
- Replace broad starter imports with
prelude::core::*where possible. - Update raw
data-state/aria-*selectors to typed helpers when the built-in enums cover the case. - Re-run:
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo check --workspace --all-features --examples
Concepts
Core concepts:
Design Tokens
Twill tokens are Rust enums and value types representing styling primitives.
Color tokens
Use Color + Scale:
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
let primary = Color::blue(Scale::S500);
let danger = Color::red(Scale::S600);
let bg = Color::slate(Scale::S50);
}
Special colors:
Color::white()Color::black()Color::transparent()
Typed arbitrary/custom-property escape hatches:
ColorValueTokenBackgroundColorVarTextColorVarBorderColorVarOutlineColorVarRingColorVarShadowColorVar
Spacing tokens
Use Spacing for paddings, margins, gaps:
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
let p = Spacing::S4; // 1rem
let gap = Spacing::S2; // 0.5rem
}
Border and radius tokens
BorderWidthBorderStyleBorderRadius
Typography tokens
FontFamilyFontSizeFontWeightLineHeightLetterSpacingTextAlign
Typography also supports typed custom values:
FontSizeVarLetterSpacingVarLineHeightVarLetterSpacing::Em(...)LineHeight::Number(...)
Semantic aliases intentionally live in a separate import layer:
#![allow(unused)]
fn main() {
use twill::prelude::theme::*;
let theme = SemanticThemeVars::shadcn_neutral();
let foreground = theme.resolve_light(SemanticColor::Foreground);
}
Shadow tokens
ShadowInsetShadowDropShadowTextShadow
Motion tokens
TransitionDurationEasingAnimationToken
Motion is applied through Style methods:
transition_property(...)transition_duration(...)transition_ease(...)animate(...)
For edge cases there are typed arbitrary/custom paths such as:
transition_custom(...)transition_duration_ms(...)blur_px(...)perspective_px(...)
Style Builder
Style is the central composition object. You combine typed tokens and utilities, then map the resulting structure to a native backend.
#![allow(unused)]
fn main() {
use twill::prelude::{arbitrary::*, core::*};
let card = Style::card()
.merged(Style::interactive())
.px_px(18.0)
.pb_rem(1.25)
.w_var(WidthVar::new("--card-width"))
.min_h_var(HeightVar::new("--card-min-h"))
.flex_arbitrary("2_1_auto")
.transition_custom("filter, transform");
}
Preset styles are the default path. Arbitrary/custom values are controlled escape hatches for layout, spacing, color, typography, and motion when a backend-specific edge case is not covered by the standard scale.
Compose reusable layers with merged(...) or merge_in_place(...) instead of inventing framework-specific wrapper structs too early.
State And Responsive Layers
Twill keeps interactive and responsive styling at the Style layer instead of shipping UI components.
Stateful styling
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
let style = Style::interactive()
.bg(Color::slate(Scale::S100))
.text_color(Color::slate(Scale::S900))
.hover(|style| style.opacity(0.9))
.selected(|style| style.bg(Color::blue(Scale::S500)))
.checked(|style| {
style.border(
BorderWidth::S1,
BorderStyle::Solid,
Color::green(Scale::S500),
)
})
.open(|style| style.shadow(Shadow::Lg))
.data_attr(DataState::Open, |style| style.text_color(Color::white()))
.aria_attr(AriaAttr::Selected, |style| style.font_weight(FontWeight::Bold));
}
Supported built-in state layers:
hoverfocusfocus_visibleactivedisabledselectedcheckedopenclosed
Supported arbitrary hooks:
data_attr(DataState::..., ...)aria_attr(AriaAttr::..., ...)
Responsive styling
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
let style = Style::card()
.w(Spacing::S12)
.at_sm(|style| style.w(Spacing::S24))
.at_lg(|style| style.h(Spacing::S32));
let resolved = style.at_breakpoint(Breakpoint::Lg);
assert_eq!(resolved.width_value(), Some(Width::from(Spacing::S24)));
assert_eq!(resolved.height_value(), Some(Height::from(Spacing::S32)));
}
Available breakpoint helpers:
smmdlgxls2xl
You can also attach layers generically with responsive(Breakpoint::..., ...).
Semantic Tokens
Semantic tokens map role-based names (background, foreground, primary, etc.) to concrete colors in light and dark palettes.
#![allow(unused)]
fn main() {
use twill::prelude::theme::*;
let theme = SemanticThemeVars::shadcn_neutral();
let light_bg = theme.resolve_light(SemanticColor::Background);
let dark_bg = theme.resolve_dark(SemanticColor::Background);
}
If you want to resolve semantic aliases before handing a style to a backend, do it on Style
directly:
#![allow(unused)]
fn main() {
use twill::prelude::{arbitrary::*, core::*, theme::*};
let theme = SemanticThemeVars::shadcn_neutral();
let style = Style::card()
.text_color_token(TextColor::semantic(SemanticColor::Primary))
.ring_token(RingWidth::S2, RingColor::semantic(SemanticColor::Ring));
let resolved = style.resolved_dark_theme(theme);
assert!(matches!(
resolved.text_color_token_value(),
Some(TextColor::Arbitrary(_))
));
}
Backends
Backend adapters:
Backends Overview
Twill core is backend-agnostic.
Adapters convert tokens/styles into framework-specific types.
Supported adapters:
eguiicedslint
The core crate stays synchronous and useful without any backend features. Only enabled adapters pull their runtime or windowing dependencies.
Enabling backend features
[dependencies]
twill = { version = "0.3", features = ["egui", "iced", "slint"] }
Backend modules
twill::backends::eguitwill::backends::icedtwill::backends::slint
Common pattern
- Build style/tokens in Twill.
- Convert via backend helper.
- Apply to widgets in that GUI framework.
egui
Enable feature:
twill = { version = "0.3", features = ["egui"] }
What you get
- color conversion helpers,
- style integration points for
eguiwidgets, - typed translation from Twill tokens into
eguiprimitives.
Use twill::backends::egui::ToEgui and the frame/color helpers to bridge Style into egui.
iced
Enable feature:
twill = { version = "0.3", features = ["iced"] }
What you get
- color/style mapping helpers for
iced, - compatibility with
icedapplication architecture, - typed translation from Twill values into
icedprimitives.
Use twill::backends::iced::ToIced together with the helper functions in this module to feed Style data into your own iced widgets, layouts, and themes.
slint
Enable feature:
twill = { version = "0.3", features = ["slint"] }
What you get
- direct
Color -> slint::Colorconversion helpers, - integration with Slint property-based styling,
- typed translation from Twill values into
slintprimitives.
Use twill::backends::slint::{ToSlint, SlintColors, SlintSpacing, SlintRadius} to bridge Twill tokens and style values into your own Slint components.
Examples
This section documents the checked-in examples/ targets for main and the 0.3.x API.
Legacy demos built around component APIs or CSS serialization belong to the 0.2.x release line and are intentionally not reproduced here.
The examples are layered:
- small token and style-builder programs for one concept at a time;
- one focused example for arbitrary/custom values in the style layer;
- backend adapter examples that show translation into runtime primitives;
- two showcase applications that assemble the same core concepts in
eguiandiced.
Tokens Example
This example shows the lowest layer of the API: typed tokens for color, spacing, typography, shadow, and their custom-value escape hatches.
- File:
examples/01_tokens.rs - Run:
cargo run --example tokens
- Expected output: a small terminal dump of palette values, spacing in pixels, representative typography/shadow tokens, and typed custom token wrappers.
Why this exists:
use it when you need to inspect or explain Twill’s primitive values before composing full Style objects.
Style Builder Example
This example shows how a reusable base Style is composed with an additional layer through merged(...), including layout and spacing escape hatches.
- File:
examples/02_style_builder.rs - Run:
cargo run --example style_builder
- Expected output: the merged padding, background, text color, width, constraints, layout tokens, radius, and shadow values.
Why this exists: it demonstrates that Twill’s core abstraction is style composition, not framework-specific components, and that arbitrary/custom values still live inside the same typed builder surface.
States Example
This example isolates state and attribute layers such as hover, focus_visible, disabled, data_attr, and aria_attr.
- File:
examples/03_states.rs - Run:
cargo run --example states
- Expected output: a terminal summary of the nested state styles stored on the composed
Style.
Why this exists: stateful styling is one of the biggest differences between raw tokens and a useful UI style system, so it deserves its own single-purpose example.
Responsive Example
This example shows breakpoint layers and how they resolve through Style::at_breakpoint(...).
- File:
examples/04_responsive.rs - Run:
cargo run --example responsive
- Expected output: one line per breakpoint showing the resolved width, height, padding, and shadow values.
Why this exists:
responsive behavior should be inspectable without launching a GUI backend.
The example also uses the more discoverable at_md/at_2xl builder aliases.
Semantic Theme Example
This example shows both the built-in SemanticThemeVars::shadcn_neutral() palette and a runtime-generated DynamicSemanticTheme.
- File:
examples/05_semantic_theme.rs - Run:
cargo run --example semantic_theme
- Expected output: light and dark semantic resolutions for key aliases like
Background,Primary,Border, andRing.
Why this exists: semantic aliases are the bridge between raw tokens and app-level themes.
Arbitrary Values Example
This example shows the controlled escape-hatch layer of twill: arbitrary and custom-property values for key utility families without falling back to a class-string parser.
- File:
examples/06_arbitrary_values.rs - Run:
cargo run --example arbitrary_values
- Expected output: a terminal dump showing arbitrary/custom tokens for background, text, spacing, constraints, border, ring, shadow color, typography overrides, and custom motion/effect values.
Why this exists: use it when preset tokens cover most of the design, but a backend-specific edge case still needs a typed arbitrary value or a custom-property hook for color, spacing, size, typography, or motion.
Backend Adapter Examples
These examples keep backend coverage focused on adapter deltas instead of repeating the full product story.
egui
- File:
examples/10_backend_egui.rs - Run:
cargo run --example backend_egui --features egui
- Expected output: converted
eguiprimitives such asColor32,Frame, cursor, semantic colors, arbitrary colors, and shadow values.
iced
- File:
examples/11_backend_iced.rs - Run:
cargo run --example backend_iced --features iced
- Expected output: converted
icedprimitives such asColor,Padding, cursor interaction, semantic colors, arbitrary colors, and shadows.
slint
- File:
examples/12_backend_slint.rs - Run:
cargo run --example backend_slint --features slint
- Expected output: converted
slintprimitives such asslint::Color, lengths, cursor wrapper, semantic colors, arbitrary colors, and shadow tuples.
Why these exist: each adapter example answers “how do I bridge Twill values into this runtime?” without pretending Twill ships a full widget kit.
If you prefer direct adapter dependencies over the facade crate, the matching crates are:
twill-eguitwill-icedtwill-slint
egui Showcase
This is the full egui showcase for main/0.3.x. It combines tokens, composed styles, state layers, responsive resolution, and semantic theme switching in one window.
- File:
examples/20_showcase_egui.rs - Run:
cargo run --example showcase_egui --features egui
- Expected UI: a native
eguiwindow with token swatches, semantic theme labels, and composed cards built from shared Twill styles.
Why this exists:
it is the visual “map” of the system for egui, while the smaller examples stay optimized for one concept at a time.
iced Showcase
This is the full iced showcase for main/0.3.x. It mirrors the conceptual content of the egui showcase while keeping the backend-specific wiring in iced.
- File:
examples/21_showcase_iced.rs - Run:
cargo run --example showcase_iced --features iced
- Expected UI: a native
icedwindow with token swatches, semantic theme sections, and composed cards rendered throughtwill::backends::iced.
Why this exists: it proves that the same Twill style story can be surfaced in a second backend without reintroducing old component abstractions into the crate.
Reference
Public API
twill keeps its public API centered on style primitives and backend adapters.
If you only need the backend-agnostic style engine, depend on twill-core directly. The
top-level twill crate is now a facade that re-exports the same core modules and optionally
adds GUI adapters.
Core entry points
Styletwill-coretwill::prelude::core::*twill::prelude::theme::*twill::prelude::arbitrary::*twill::prelude::traits::*twill::prelude::*MergeComputeValueResponsive
Tokens
Main token families:
Color,ColorFamily,Scale,ColorValueBackgroundColor,TextColor,BorderColor,OutlineColor,RingColorColorValueTokenand*ColorVarwrappers for typed arbitrary/custom-property valuesSemanticColor,SemanticThemeVars,DynamicSemanticThemeThemeVariantSpacing,Percentage,Container,BreakpointFontFamily,FontSize,FontWeight,LetterSpacing,LineHeightBorderRadius,BorderWidth,BorderStyle,OutlineStyle,RingWidthShadow,InsetShadow,DropShadow,TextShadowAnimationToken,TransitionDuration,TransitionProperty,Easing
Utilities
Common utility-style value types:
Padding,PaddingValue,PaddingVarMargin,MarginValue,MarginVarWidth,WidthVarHeight,HeightVarSizeConstraintsDisplay,Position,Overflow,ZIndexFlexContainer,FlexDirection,FlexGridContainer,GridTemplate
Recommended usage
#![allow(unused)]
fn main() {
use twill::prelude::core::*;
let card = Style::card()
.merged(Style::interactive())
.padding(Padding::all(Spacing::S4))
.hover(|style| style.shadow(Shadow::Md))
.at_lg(|style| style.padding(Padding::all(Spacing::S6)));
assert!(!card.is_empty());
}
Composition helpers
Prefer composing Style values instead of creating framework-specific wrapper types:
Style::surface()for structural spacing, radius, and elevationStyle::card()for semantic card/background/border defaultsStyle::interactive()for pointer/focus/disabled affordancesStyle::merged(...)when you want an immutable composition stepStyle::merge_in_place(...)when you want to extend a mutable style valueStyle::resolved_theme(...)plusresolved_light_theme(...)/resolved_dark_theme(...)when you want semantic aliases converted into concrete color tokensStyle::resolve_theme_in_place(...)when you want that resolution on a mutable style- Verbose builder aliases like
background_color(...),border_color(...),outline_color(...),ring_width(...),ring_color(...), andbox_shadow(...)when you want a more self-explanatory Rust-first call site
The wide twill::prelude::* remains available, but prelude::core::* plus opt-in
sub-preludes gives better IDE discoverability and smaller import scope.
Backend surface
Feature-gated backend modules:
twill::backends::eguitwill::backends::icedtwill::backends::slinttwill::eguitwill::icedtwill::slint
These modules convert Twill values into framework-specific primitives. Twill itself does not define components like Button or Card.
If you do not want the facade crate, use the dedicated adapter crates directly:
twill-eguitwill-icedtwill-slint
Backend-specific helper types:
twill::backends::ShadowColortwill::iced::TextDirectiontwill::slint::SlintCursor