egui_styled
A styling layer for egui that adds per-widget hover/focus/active styling, semantic design tokens, and composable style functions. Tailwind-style utility styling for an immediate-mode toolkit, with Flutter-shaped widgets.
Build UI thinking about what things look like, rather than how egui's internals work.
Why?
egui's styling lives on ui.visuals_mut(), which is global to the current Ui. Applying a custom hover color to one button means cloning the visuals, mutating three WidgetVisuals states, and wrapping in ui.scope every time on every widget. The pattern is correct but is tricky to scale.
egui_styled wraps egui's widgets with builder APIs that handle the visuals dance for you, and adds a small theme system so colors/spacing/radii live in one place.
Before / After
A card with two themed text inputs and two buttons.
With egui_styled
use *;
// One line fetches both. `WebPalette` is the opt-in starter color set;
// swap for your own struct (game palette, IDE syntax theme, etc).
let = ui.ctx.;
frame
.bg
.corner_radius
.padding
.border
.show;
ui.scope;
Same UI, ~50 lines vs ~30 and the raw-egui side is mostly visuals_mut copy-paste. Per-widget hover/focus colors. Once you start composing styles, the duplication on the After side collapses too.
Another common pattern: a blinking prompt that must not reflow surrounding layout. With raw egui you reach for allocate_exact_size + painter().text(); with egui_styled it's two builder methods:
// Raw egui
let = ui.allocate_exact_size;
if visible
// egui_styled
label
.font
.text_color
.min_height // reserves the slot every frame
.visible // skips painting without collapsing space
.show;
Fair comparison: you can also factor the raw-egui side into helper functions, which closes most of the line gap (~25 vs ~20). The bigger remaining win is what kind of helper you can write: in raw egui, your helper has to take
&mut Uiand render immediately. Inegui_styleda style helper returnsimpl Fn(W) -> W, a pure value you can store, compose with other helpers, or tweak per call site (.apply(primary_button(&t)).margin_top(8.0)). Uniformity matters too: every helper inegui_styledhas the same shape regardless of whether it styles a button, frame, or input.
Features
| Status | Feature |
|---|---|
| ✅ | StyledButton with bg / hover_bg / active_bg / text_color / border / corner_radius |
| ✅ | StyledLabel with font_size, bold, italics, wrap_mode (wrap / truncate / extend) |
| ✅ | StyledTextEdit with hint, password, multiline, focus state styling |
| ✅ | StyledCheckbox with full pseudo-state support |
| 🚧 | StyledSlider generic over T: Numeric, but track/handle styling is shallow |
| 🚧 | StyledComboBox trigger styled, popup items inherit |
| ✅ | StyledFrame with bg / border / padding / margin / corner_radius |
| ✅ | StyledRow / StyledColumn containers with gap support |
| ✅ | StyledStack overlay container (layered children, offsets, alignment) |
| ✅ | SharedStyle resolver, hover/focus/active falls through to egui defaults |
| ✅ | PseudoState tracking via egui::Memory (1-frame lag, imperceptible) |
| ✅ | StyledTheme design tokens (colors / spacing / radii / typography) |
| ✅ | ThemeExt for egui::Context (ctx.set_styled_theme() / ctx.styled_theme()) |
| ✅ | Apply trait for composable style functions |
| ⚒️ | Style newtype as data (build once, merge into any widget) [future work] |
| ⚒️ | Snapshot/visual regression tests [future work] |
Installation
[]
= "0.1"
= "0.34"
Design tokens
egui_styled ships geometry/typography tokens in [StyledTheme] (universally useful - spacing, radii, font sizes, font families) and an optional starter color palette in [WebPalette] (semantic web-style color names). Colors are inherently domain-specific, so the library doesn't force its vocabulary on you - for games, IDEs, or anything else, define your own struct and store it via the generic DesignSlots mechanism.
Geometry (StyledTheme)
use ;
use *;
Pair the family tokens with the size scale at the call site via the helper methods:
label
.font
.show;
Colors - Option A: use the starter palette
If your app fits a web/dashboard vocabulary (accent, error, warning, success, fg_on_accent, etc.), use [WebPalette]:
use Color32;
use *;
Colors - Option B: define your own
If your app has domain-specific colors that don't fit web semantics (game HUDs, IDE syntax, etc.), define your own struct and store it the same way:
use ;
use *;
Storing and reading
Both StyledTheme and any user-defined type are stored on egui::Context via the same primitive:
// Once at startup
ctx.set_styled_theme;
ctx.set_design_data; // or ArcadeColors { ... }
// Anywhere in your UI
let t = ui.ctx.styled_theme;
let p = ui.ctx.; // or ::<ArcadeColors>()
DesignSlots is the underlying typed-storage trait - one slot per TypeId. ThemeExt::set_styled_theme / styled_theme are convenience wrappers over it. If you need two slots of the same underlying type (two Vec<Color32> palettes, etc.), newtype them.
See examples/theme_demo.rs for two themes (midnight, parchment) you can use as starting points.
Composing styles
Reuse styling across call sites with the Apply trait. Since colors and geometry are separate, helpers typically close over both:
+ 'static
button.apply.show;
Apply is implemented for every styled type and is in the prelude.
The library doesn't ship preset helpers like primary_button - what's "primary" is a product decision, not a library one. Define them in your app the way above, alongside whatever color type you've chosen.
Overlays (Styled::stack())
row and column are thin wrappers over egui's flow Layout, so they can't put two things in the same pixels. Styled::stack() is the exception - an overlay container where every layer shares one origin and paints on top of the previous one (first layer = bottom, last = top). You'd otherwise have to drop down to raw painter() calls to achieve the z-index style effect: layered backgrounds, badges, "centered text over an image", or ghost/chromatic-aberration effects.
// Chromatic-aberration "[ENTER]": three offset layers.
stack
.layer_offset
.layer_offset
.layer
.show;
// Overlay aligned within the first layer (e.g. centered over a background).
stack
.layer
.layer_aligned
.show;
.layer(fn)/.layer_offset(offset, fn)anchor at the shared origin (optionally nudged by a pixel offset)..layer_aligned(align, fn)positions a layer within the union of all preceding layers - so "background first, overlay aligned on it" works..layer_fixed(size, align, fn)contributes onlysizeto the stack's allocation; content is positioned within that box viaalignand may overflow freely. Use for scale-punch / bounce / pop animations where the content briefly renders larger than its resting size without pushing siblings..sense(Sense::click())makes the whole stack report interaction; defaults to hover.
Because layers are painted before the stack's final position is known (it's the parent that decides, especially under centering), the stack paints at a provisional spot and then translates its own shapes into place. Two practical notes: .extend() on labels (see below) avoids truncation when a layer is laid out tight, and interactive widgets inside a stack are hit-tested at the pre-translation position - fine for the labels/visuals this is meant for, but don't bury a button deep inside a centered stack.
.extend()-StyledLabelexposes egui's fullTextWrapModevia.wrap_mode(...), plus.wrap()/.truncate()/.extend()shortcuts..extend()("lay out at natural width, never wrap or truncate") is what keeps text intact inside tight rows and stacks.
Examples
Performance
Per styled widget the overhead vs raw egui is approximately:
- 1
ui.scope(which egui itself uses constantly internally) - 2
egui::Memorylookups (pseudo-state load/store) - 1
SharedStyle::resolve(a branch chain overOptions)
Mostly stack work, plus a few small heap allocations per widget: a Visuals clone, the scope's child Ui state, occasional short strings when a font override round-trips through RichText on text widgets.
Status
Pre-1.0. The API surface is functional but not used in anger. Expect rough edges, especially around slider and combo box styling. Feedback and bug reports welcome.
License
MIT or Apache-2.0, at your option.