Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-core stays backend-agnostic and does not require a GUI runtime.
  • twill-egui, twill-iced, and twill-slint each depend only on twill-core plus their own runtime crate.
  • Base twill simply re-exports twill-core; it does not require a GUI runtime until you enable a backend feature.
  • egui enables egui conversion helpers only.
  • iced enables the Iced adapter and the Linux windowing/runtime feature set used by this crate configuration.
  • slint enables 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::* for Merge, 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

  • Style is the center of the public API.
  • Backend adapters stay feature-gated and convert Twill values into egui, iced, and slint types.
  • Legacy component demos and CSS-oriented APIs belong to the 0.2.x line.

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::..., ...) and aria_attr(AriaAttr::..., ...) over raw strings.
  • Prefer Style::at_breakpoint(...) in normal application code.
  • Use at_sm / at_md / at_lg / at_xl / at_2xl if the short breakpoint builders feel too terse.
  • Prefer Style::surface(), Style::card(), and Style::interactive() as reusable starting points.
  • Prefer merged(...) or merge_in_place(...) for explicit style composition.

What did not change

  • The short DSL remains available.
  • Raw escape hatches like data_state("...") and aria_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:

  • ColorValueToken
  • BackgroundColorVar
  • TextColorVar
  • BorderColorVar
  • OutlineColorVar
  • RingColorVar
  • ShadowColorVar

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

  • BorderWidth
  • BorderStyle
  • BorderRadius

Typography tokens

  • FontFamily
  • FontSize
  • FontWeight
  • LineHeight
  • LetterSpacing
  • TextAlign

Typography also supports typed custom values:

  • FontSizeVar
  • LetterSpacingVar
  • LineHeightVar
  • LetterSpacing::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

  • Shadow
  • InsetShadow
  • DropShadow
  • TextShadow

Motion tokens

  • TransitionDuration
  • Easing
  • AnimationToken

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:

  • hover
  • focus
  • focus_visible
  • active
  • disabled
  • selected
  • checked
  • open
  • closed

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:

  • sm
  • md
  • lg
  • xl
  • s2xl

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:

  • egui
  • iced
  • slint

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::egui
  • twill::backends::iced
  • twill::backends::slint

Common pattern

  1. Build style/tokens in Twill.
  2. Convert via backend helper.
  3. 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 egui widgets,
  • typed translation from Twill tokens into egui primitives.

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 iced application architecture,
  • typed translation from Twill values into iced primitives.

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::Color conversion helpers,
  • integration with Slint property-based styling,
  • typed translation from Twill values into slint primitives.

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 egui and iced.

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, and Ring.

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 egui primitives such as Color32, 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 iced primitives such as Color, 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 slint primitives such as slint::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-egui
  • twill-iced
  • twill-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 egui window 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 iced window with token swatches, semantic theme sections, and composed cards rendered through twill::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

  • Style
  • twill-core
  • twill::prelude::core::*
  • twill::prelude::theme::*
  • twill::prelude::arbitrary::*
  • twill::prelude::traits::*
  • twill::prelude::*
  • Merge
  • ComputeValue
  • Responsive

Tokens

Main token families:

  • Color, ColorFamily, Scale, ColorValue
  • BackgroundColor, TextColor, BorderColor, OutlineColor, RingColor
  • ColorValueToken and *ColorVar wrappers for typed arbitrary/custom-property values
  • SemanticColor, SemanticThemeVars, DynamicSemanticTheme
  • ThemeVariant
  • Spacing, Percentage, Container, Breakpoint
  • FontFamily, FontSize, FontWeight, LetterSpacing, LineHeight
  • BorderRadius, BorderWidth, BorderStyle, OutlineStyle, RingWidth
  • Shadow, InsetShadow, DropShadow, TextShadow
  • AnimationToken, TransitionDuration, TransitionProperty, Easing

Utilities

Common utility-style value types:

  • Padding, PaddingValue, PaddingVar
  • Margin, MarginValue, MarginVar
  • Width, WidthVar
  • Height, HeightVar
  • SizeConstraints
  • Display, Position, Overflow, ZIndex
  • FlexContainer, FlexDirection, Flex
  • GridContainer, GridTemplate
#![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 elevation
  • Style::card() for semantic card/background/border defaults
  • Style::interactive() for pointer/focus/disabled affordances
  • Style::merged(...) when you want an immutable composition step
  • Style::merge_in_place(...) when you want to extend a mutable style value
  • Style::resolved_theme(...) plus resolved_light_theme(...) / resolved_dark_theme(...) when you want semantic aliases converted into concrete color tokens
  • Style::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(...), and box_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::egui
  • twill::backends::iced
  • twill::backends::slint
  • twill::egui
  • twill::iced
  • twill::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-egui
  • twill-iced
  • twill-slint

Backend-specific helper types:

  • twill::backends::ShadowColor
  • twill::iced::TextDirection
  • twill::slint::SlintCursor