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

Hover & Pressed States

Define visual changes for different interaction states.

Every value inside a state layer is reactive, like the base property it covers. when_hovered(|s| s.background(theme.accent)) follows the theme; it does not take a snapshot of it when the widget is built.

Hover State

Applied when the mouse cursor is over the widget:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .background(Color::rgb(0.2, 0.2, 0.3))
    .when_hovered(|s| s.lighter(0.1))
;
}

Common Hover Patterns

Lighten background:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_hovered(|s| s.lighter(0.1))
;
}

Explicit color change:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_hovered(|s| s.background(Color::rgb(0.4, 0.6, 0.9)))
;
}

Border highlight:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.border(1.0, Color::rgb(0.3, 0.3, 0.4))
.when_hovered(|s| s.border(1.0, Color::rgb(0.5, 0.5, 0.6)))
;
}

The width stays: a border takes room inside the box, so a hover that widened it would move the content by the difference.

Ring:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.outline(2.0, Color::TRANSPARENT, 2.0)
.when_hovered(|s| s.outline(2.0, Color::rgb(0.5, 0.5, 0.6), 2.0))
;
}

An outline is drawn outside the box and takes no room, so showing it moves nothing.

Shadow lift:

extern crate guido;
use guido::prelude::*;
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.shadow(LOW)
.when_hovered(|s| s.shadow(RAISED))
;
}

Pressed State

Applied when the mouse button is held down:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .background(Color::rgb(0.2, 0.2, 0.3))
    .when_pressed(|s| s.darker(0.1))
;
}

Common Pressed Patterns

Darken background:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.darker(0.1))
;
}

Scale down (tactile feedback):

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.scale(0.98))
;
}

Shrink the shadow (press into the surface):

extern crate guido;
use guido::prelude::*;
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.shadow(RAISED)
.when_pressed(|s| s.shadow(LOW))
;
}

Ripple effect:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.ripple())
;
}

Combining Hover and Pressed

Most interactive elements use both states:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .background(Color::rgb(0.2, 0.2, 0.3))
    .when_hovered(|s| s.lighter(0.1))
    .when_pressed(|s| s.ripple().darker(0.05))
;
}

Text Colour

A state layer can change the colour of the text below the container, not just the box around it:

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
container()
    
    .control()   // the text below declares .when_hovered(|s| s.color(theme.text))
    .child(text("Label").color(theme.text_weak))
;
}

This works the same way the ordinary declaration does — the container publishes its text colour to descendants, and here what it publishes folds in the hover. A text that declares its own colour nearer down is unaffected, and a container with no state layer mentioning text creates nothing extra.

The base it returns to when the state ends is the colour that would have been inherited anyway, so a container can override on hover without restating what its ancestors already said:

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
container()
                  // set once, further up
    .child(
        container()
            .control()   // the text below declares .when_hovered(|s| s.color(theme.text))
            .child(text("Label").color(theme.text_weak)),            // weak, then strong, then weak
    )
;
}

It cannot be animated. A container transitions background, border_color, border_width, corners, shadow, width, height, padding, translate, rotate and scale; text colour is not among them, so the override lands in one frame.

A transition declared on two levels — an animated value whose own base comes from an ancestor that is itself animating — retargets every frame, which gives a damped chase rather than a transition with its own curve. It converges either way; CSS instead starts the inner one once, towards the outer’s final value.

States Your App Owns

Hover, pressed and focus are noticed by the container. The rest — the last submit failed, this row is selected, this value is out of range — are conditions your app already holds, so there is nothing to propagate: pass the signal and it is read where the style is resolved.

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let password = create_password();
let theme = Theme::default();
let wrong_password = create_signal(false);

container()
    .border(1.0, theme.line)
    .state(wrong_password, |s| s.border(1.0, theme.error))
    .child(password_input(password))
;
}

Because it is just a signal, anything else that needs it reads the same one, independently — a label beside the field does not have to be told:

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
let wrong_password = create_signal(false);
container()
    .state(wrong_password, |s| s.border(1.0, theme.error))
    .child(text("Wrong password"))
;
}

Whose Hover?

A label inside a button has to light up while the pointer is on the button’s padding, nowhere near the glyphs. And it has to stay dark while the pointer is over a different button in the same row. So neither “my own bounds” nor “any ancestor” is the answer.

The answer is the nearest enclosing control — the interaction unit the widget belongs to:

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let save = move || {};
let theme = Theme::default();
container()
    .padding(12.0)
    .on_click(save)
    .child(text("Save").color(theme.weak).when_hovered(|s| s.color(theme.strong)))
;
}

on_click makes that container a control, because anything the pointer can act on is a unit by necessity. So do on_hover, on_scroll, scroll and any declared state layer. Write control() yourself where the boundary is real but nothing else announces it:

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let password = create_password();
let theme = Theme::default();
container().control()
    .child(text("Password").when_focused(|s| s.color(theme.accent)))
    .child(password_input(password))
;
}

That is the case the old rule could not express at all: the focus is in a sibling, not below the label. Both belong to the same unit, so the label can ask about it.

Leaves get when_hovered, when_pressed, when_focused and state from the Stateful trait, and the closure hands back the same builder the widget itself uses.

Nesting scopes resolution, not state. A list row stays hovered while the pointer is over a button nested in it — the pointer is plainly still on the row. What the nested control changes is only who a descendant asks.

With nothing marked above it, a widget is its own unit and notices the pointer over its own bounds. It is never pressed: being pressed means being activated, and it has nothing to activate.

when_focused also keeps the focus. A press that no widget claimed takes the keyboard off whatever held it, and a press inside a container declaring when_focused while that focus is inside it is not one of those. A box that says it lights up for a field has said the box is part of the field, so clicking its padding does not blur what it draws.

It keeps the focus without consuming the press. A clickable row wrapping a decorated field still sees the click, still lights its pressed layer and still fires its on_click — keeping the keyboard and swallowing the event are two different things. See Losing Focus.

Which Layer Wins

Layers are resolved in reverse declaration order, one property at a time: the last one you wrote that is active and speaks about that property wins.

This is what lets a field say that its error outranks its focus ring — which matters on a password field, where the focus is held essentially all the time:

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let password = create_password();
let theme = Theme::default();
let wrong_password = create_signal(false);
container()
    .border(1.0, theme.line)
    .outline(2.0, Color::TRANSPARENT, 2.0)
    .when_focused(|s| s.outline(2.0, theme.accent, 2.0))
    .state(wrong_password, |s| s.outline(2.0, theme.error, 2.0))   // written after,
    .child(password_input(password))                                    // so it wins
;
}

Swap the two lines and the focus ring wins instead. A layer that says nothing about a property is skipped rather than ending the search, so the pressed layer below only takes over the transform and leaves the hover’s background alone:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .when_hovered(|s| s.lighter(0.1))
    .when_pressed(|s| s.scale(0.98))
;
}

Keep in mind that a layer replaces the base rather than ranking against it. A border that already carries a meaning does not belong in the base — put it in a layer with a condition, where the ordering above can speak about it.

Combining Multiple Overrides

Each state can override multiple properties:

extern crate guido;
use guido::prelude::*;
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.when_hovered(|s| s
    .lighter(0.1)
    .outline(2.0, Color::rgb(0.5, 0.7, 1.0), 2.0)
    .shadow(RAISED)
)

.when_pressed(|s| s
    .ripple()
    .darker(0.05)
    .scale(0.98)
    .shadow(LOW)
)
;
}

With Animations

Add transitions for smooth state changes:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .background(Color::rgb(0.3, 0.5, 0.8).transition(200.0))
    .outline(2.0, Color::TRANSPARENT.transition(150.0), 2.0)
    .scale(Scale::NONE.transition(Transition::spring(SpringConfig::GENTLE)))
    .when_hovered(|s| s.lighter(0.1).outline(2.0, Color::WHITE, 2.0))
    .when_pressed(|s| s.darker(0.1).scale(0.98))
;
}

Button Patterns

Simple Button

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .padding(12.0)
    .background(Color::rgb(0.3, 0.5, 0.8))
    .corners(6.0)
    .when_hovered(|s| s.lighter(0.1))
    .when_pressed(|s| s.ripple())
    .on_click(|| println!("Clicked!"))
    .child(container().child(text("Click me").color(Color::WHITE)))
;
}

Outlined Button

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .padding(12.0)
    .background(Color::TRANSPARENT)
    .corners(6.0)
    .border(1.0, Color::rgb(0.5, 0.5, 0.6))
    .when_hovered(|s| s.background(Color::rgba(1.0, 1.0, 1.0, 0.1)))
    .when_pressed(|s| s.ripple())
    .child(container().child(text("Outlined").color(Color::WHITE)))
;
}

Card with Lift

// not compiled: `[...]` stands in for the children; this block is about
// the shadows above them.
extern crate guido;
use guido::prelude::*;
const FLAT: Shadow = Shadow::none();
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
    .padding(16.0)
    .background(Color::rgb(0.15, 0.15, 0.2))
    .corners(8.0)
    .shadow(LOW.transition(200.0))
    .when_hovered(|s| s.shadow(RAISED).lighter(0.03))
    .when_pressed(|s| s.shadow(FLAT))
    .children([...])
;
}

API Reference

StateStyle Builder

// not compiled: a signature listing — these declarations have no bodies.
impl StateStyleBuilder {
    // Background
    pub fn background(self, color: Color) -> Self;
    pub fn lighter(self, amount: f32) -> Self;
    pub fn darker(self, amount: f32) -> Self;
    pub fn background_alpha<M>(self, alpha: impl IntoSignal<f32, M>) -> Self;

    // The whole container, children included — its own `opacity`
    pub fn opacity<M>(self, opacity: impl IntoSignal<f32, M>) -> Self;

    // Border — both halves, always; half a border is no border
    pub fn border(self, width: f32, color: Color) -> Self;

    // Outline — width, colour and offset, always; it takes no room
    pub fn outline(self, width: f32, color: Color, offset: f32) -> Self;

    // Other
    pub fn corners<M>(self, corners: impl IntoSignal<Corners, M>) -> Self;
    // Every value is reactive here too, like the container's own
    pub fn translate<M>(self, translate: impl IntoSignal<Translate, M>) -> Self;
    pub fn rotate<M>(self, degrees: impl IntoSignal<f32, M>) -> Self;
    pub fn scale<M>(self, factor: impl IntoSignal<Scale, M>) -> Self;
    pub fn shadow<M>(self, shadow: impl IntoSignal<Shadows, M>) -> Self;

    // Ripple
    pub fn ripple(self) -> Self;
    pub fn ripple_with_color<M>(self, color: impl IntoSignal<Color, M>) -> Self;
}