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.
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 |
| ✅ | 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 |
| ✅ | 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.
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.