Modifiers
Variants
Restyle an element while it is hovered, pressed, focused, disabled or dark, and animate it in and out.
Element::hoveruse crownui::prelude::*;
fn primary(label: &'static str) -> impl View {
button(label)
.bg(rgb(0x3b82f6))
.text_color(WHITE)
.hover(|style| style.bg(rgb(0x2563eb)))
.active(|style| style.bg(rgb(0x1d4ed8)).scale(0.97))
.focus_visible(|style| style.ring(2.0, rgb(0x93c5fd)))
.when_disabled(|style| style.opacity(0.4))
.dark(|style| style.bg(rgb(0x60a5fa)).text_color(rgb(0x0f172a)))
.transition(Spring::snappy())
}impl<K: Kind> Element<K> {
pub fn hover(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn active(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn focus(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn focus_visible(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn when_disabled(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn dark(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn enter(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn exit(self, style: impl FnOnce(VariantStyle) -> VariantStyle) -> Self
pub fn transition(self, curve: impl Into<Curve>) -> Self
}Variants restyle an element while a state holds: hovered, pressed, focused, disabled or in dark mode. They are Tailwind's hover:, active:, focus:, focus-visible:, disabled: and dark: prefixes. enter, exit and transition on the same type control how those changes animate.
Each variant method takes a closure that receives an empty VariantStyle and returns it with styles added. VariantStyle implements Styled, so you call the same methods you use on the element. The styles apply on top of the element's base style while the state holds, and they are removed when it ends.
Variants are methods of Element<K>, the type every widget returns. They are not part of Styled, so you cannot nest a variant inside another variant.
Parameters
Builds the styles for the state. It runs once, when you call the method. Values inside it are literals or bound sources such as bind(..).
For transition only. A Spring or a Curve. See Spring and Curve.
Returns
The same element with one more modifier, ready for more chained calls.
hover
Applies while the pointer is over the element or any of its children.
text("Row").p(8.0).rounded(6.0).hover(|style| style.bg(rgba(0x0f172a0d)))active
Applies while the element is pressed, from pointer down until pointer up. A press goes to the innermost element under the pointer that has a click listener and is not disabled, so the element needs an on_click.
button("Press")
.on_click(|_cx, _| println!("pressed"))
.active(|style| style.scale(0.96))
.transition(Spring::crisp())focus
Applies while the element has keyboard focus, whether the focus came from a click or from the keyboard.
let query = cx.signal(String::new());
text_field(query, "Search").focus(|style| style.bg(WHITE).ring(1.0, rgb(0x3b82f6)))focus_visible
Applies while the element has focus that arrived by keyboard, such as Tab. Clicking a button does not show it. Use it for focus rings, so mouse users do not see a ring after every click.
button("Save").focus_visible(|style| style.ring(2.0, rgb(0x3b82f6)))when_disabled
Applies while the element is disabled through .disabled(..) from the Semantic trait. Disabling also blocks clicks and makes agents and tests get the error CUI0106 when they act on the node. The method is called when_disabled because disabled is the method that sets the state.
let draft = cx.signal(String::new());
button("Send")
.disabled(bind(move |rt| draft.with(rt, String::is_empty)))
.when_disabled(|style| style.opacity(0.4))dark
Applies while the window's color scheme is ColorScheme::Dark. Set the scheme with AppConfig::color_scheme or by writing to cx.color_scheme(). See dark mode.
vstack(text("Card")).bg(WHITE).dark(|style| style.bg(rgb(0x1e293b)).text_color(WHITE))enter
Gives the style the element starts from when it is first mounted. It then animates to its real style.
vstack(text("Saved")).p(12.0).enter(|style| style.opacity(0.0).translate_y(12.0))exit
Gives the style the element animates to when it is removed. It stays on screen until the animation ends, then it is freed. Only the root of the removed subtree uses its exit style, and its children leave with it.
vstack(text("Saved")).p(12.0).exit(|style| style.opacity(0.0).scale(0.95))enter and exit animate with the element's transition or the nearest one above it. Without one, they use Spring::smooth(). Both read only the paint and transform properties of the style, such as opacity, scale, translate and background.
transition
Animates every change of this element and of every element inside it with curve. A child's own transition takes precedence over the one it inherits. Without a transition, changes apply at once.
vstack((text("One"), text("Two"))).transition(Spring::smooth())The VariantStyle type
VariantStyle collects the styles of one variant. It has every Styled method. It filters what it keeps by the kind of property.
| Kind | Methods | In a state variant | In enter and exit |
|---|---|---|---|
| Paint | bg, border, border_color, ring, outline, rounded and the per corner forms, shadow, shadow_none, opacity, backdrop_blur, hidden, z, top_layer | Applied | Applied |
| Transform | translate, translate_x, translate_y, scale, scale_x, scale_y, rotate, origin | Applied | Applied |
| Text | text_size, text_color, text_blur, font_weight, font_medium, font_semibold, font_bold, font_family, italic, leading, tracking, text_left, text_center, text_right, text_justify, truncate, line_clamp | Applied | No effect |
| Interaction | cursor, material, pointer_events_none, pointer_events_auto | Kept, with no effect | No effect |
| Layout | padding, margin, sizes, aspect, flex, gap, alignment, grid, position and insets, display_none, overflow_hidden, overflow_scroll | Ignored with CUI0201 | Ignored with CUI0201 |
Set a cursor, a material or pointer events on the element itself, or bind them to a signal.
Diagnostics
- CUI0201: a layout property was used in a variant. It is ignored and logged as a warning. The same happens when a bound value inside a variant returns a layout property.
- CUI0202: a modifier that is not a style was used in a variant, such as
style.on_click(..)orstyle.label(..). It is ignored.enterandexitalso log CUI0202 for a bound value, because they capture their styles once and take literals only.
Both codes are listed in errors.
// CUI0201: padding is a layout property, the hover keeps the old padding
button("Bad").hover(|style| style.p(20.0))
// Works: scale changes the drawn size without layout
button("Good").hover(|style| style.scale(1.05))Order and precedence
Variants always apply on top of the base style, whatever order you call them in. When two active variants set the same property, the one you declared later wins. A pressed button is also hovered, so declare hover before active to let the pressed color show.
button("Order")
.hover(|style| style.bg(rgb(0x2563eb)))
.active(|style| style.bg(rgb(0x1d4ed8))) // wins while pressedBound values in variants
State variants accept bound values, so a hover color follows a theme.
let accent = cx.signal(rgb(0x3b82f6));
button("Themed").hover(move |style| style.bg(bind(move |rt| accent.get(rt))))The binding runs as its own observer and updates the variant's value when accent changes, even while the variant is not active.
Why variants work this way
The UI thread tracks hover, press, focus and disabled for each element. When one changes, it recomputes the drawn style of that one element from its base style and its active variants and sends the result to the render thread. No closure runs, no signal flushes and layout does not run. With a transition, the render thread animates the change on its own clock.
That design is why variants refuse layout properties. A hover that changed padding would move siblings and run layout on every pointer move across a list. Keeping variants to paint, transforms and text keeps a hover to one recomputed style and one message.
It has these costs:
- A variant cannot grow or move a box in the layout. Use
scaleortranslatefor the look, or bind a size to a signal that a listener sets. - There is no
group-hover. A parent's hover does not restyle its children through variants. Set a signal fromon_pointer_enterandon_pointer_leaveon the parent and bind the children to it. - Text properties in a variant reshape the text, which runs layout for that text. Prefer color and opacity changes for frequent states such as hover.