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

Ripple Effects

Ripples provide Material Design-style touch feedback. They expand from the click point and create a visual acknowledgment of user interaction.

Basic Ripple

Add a default ripple to the pressed state:

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

The default ripple uses a semi-transparent white overlay.

Colored Ripple

Customize the ripple color:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.ripple_with_color(Color::rgba(1.0, 0.8, 0.0, 0.4)))
;
}

Good ripple colors have transparency (alpha 0.2-0.5):

extern crate guido;
use guido::prelude::*;
fn main() {
// Subtle white
Color::rgba(1.0, 1.0, 1.0, 0.2);

// Yellow accent
Color::rgba(1.0, 0.8, 0.0, 0.4);

// Blue accent
Color::rgba(0.3, 0.5, 1.0, 0.3)
;
}

Ripple with Other Effects

Combine ripples with other pressed state changes:

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

How Ripples Work

  1. Press - a disc appears at the click point, already at about a third of its final size, and starts spreading
  2. Release inside - the press happened, so the remaining expansion is completed rather than interrupted, while the disc fades out
  3. Release elsewhere, or drag off the button - nothing was activated, and the ripple says the same thing on_click does: the disc just fades, quickly

Two properties are worth stating outright, because they are what makes the effect read as an acknowledgement rather than a hesitation:

  • The radius never goes backwards. The exit is a fade, never a contraction.
  • A short press does not truncate the expansion. A click lasts 60-150ms against a growth measured in hundreds, so the release finishes the growth instead of abandoning it half-way.
  • No press is dimmer for being quicker. The disc always finishes appearing before it starts to leave, so a 20ms tap is as bright as a held one.

The disc’s centre also drifts toward the container’s own centre as it grows, so a press near a corner settles onto the button instead of staying lopsided. Its final size comes from the container, not from where you pressed — every ripple on a given button is the same size, and a full one covers it exactly.

The ripple:

  • Respects corner radius and container shape
  • Works correctly with transformed containers (rotated, scaled)
  • Renders in the overlay layer (on top of content)

Several at once

Each press is its own ripple, and they overlap:

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

Clicking this repeatedly layers one disc over the next, because two clicks are two events — the second does not erase the first. Up to four are alive at a time; past that the oldest is dropped, which is the one furthest through its own fade.

Timing

PhaseDurationScaled by
Opacity rise on contact75msfade_speed
Growth while held1sexpand_speed
Growth remaining after release225msexpand_speed
Fade after a release375msfade_speed
Fade after an abandoned press75msfade_speed

The fade never starts before the rise has finished, and the growth after a release is never given longer than the fade it overlaps — so no combination of the two speeds can put the disc back where it started, half-grown and already invisible.

Both speeds are clamped to a sane range, so a zero or a negative one degrades instead of stalling the frame loop.

The colour is a Signal<Color>, as every other declared value is, so it is built through RippleConfig::with_color — which takes a value, a closure or a signal — rather than assigned as a bare Color. The two speeds stay plain numbers:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.ripple_config(RippleConfig {
    expand_speed: 1.5,   // faster growth
    fade_speed: 1.0,
    ..RippleConfig::with_color(Color::rgba(1.0, 1.0, 1.0, 0.3))
}))
;
}

Ripples on Transformed Containers

Ripples work correctly even with transforms:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .padding(16.0)
    .background(Color::rgb(0.4, 0.6, 0.4))
    .corners(8.0)
    // Fixed order: the offset is in the parent's frame, and the box turns
    // where it has been moved to.
    .translate((10.0, 15.0))
    .rotate(5.0)
    .when_hovered(|s| s.lighter(0.1))
    .when_pressed(|s| s.ripple())
;
}

Click coordinates are properly transformed to local container space.

Ripples with Corner Curvature

Ripples respect different corner styles:

extern crate guido;
use guido::prelude::*;
fn main() {
// Squircle ripple
container()
    .corners(Corners::squircle(12.0))
    .when_pressed(|s| s.ripple());

// Beveled ripple
container()
    .corners(Corners::bevel(12.0))
    .when_pressed(|s| s.ripple())
;
}

Complete Example

extern crate guido;
use guido::prelude::*;
fn ripple_button(label: &str, color: Color) -> Container {
    container()
        .padding(16.0)
        .background(color)
        .corners(8.0)

        // Subtle hover, ripple on press
        .when_hovered(|s| s.lighter(0.1))
        .when_pressed(|s| s.ripple().scale(0.98))

        .on_click(|| println!("Clicked!"))
        .child(container().child(text(label).color(Color::WHITE)))
}

fn main() {
// Usage
ripple_button("Default Ripple", Color::rgb(0.3, 0.5, 0.8));
ripple_button("Action Button", Color::rgb(0.8, 0.3, 0.3));
}

Ripple Color Guidelines

BackgroundRipple Color
DarkColor::rgba(1.0, 1.0, 1.0, 0.2-0.3)
LightColor::rgba(0.0, 0.0, 0.0, 0.1-0.2)
ColoredLighter tint with 0.3-0.4 alpha

API Reference

extern crate guido;
use guido::prelude::*;
fn main() {
let a = 1.0f32;
let b = 0.5f32;
let g = 0.5f32;
let r = 0.5f32;
container()
// Default semi-transparent white ripple
.when_pressed(|s| s.ripple())

// Custom colored ripple
.when_pressed(|s| s.ripple_with_color(Color::rgba(r, g, b, a)))
;
}