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

Shadows

A shadow is four numbers: an offset, a blur, a spread and a colour. Shadow carries all four, and .shadow(..) is the only way to ask for one — or for several, and for one cast inside the box rather than outside it.

extern crate guido;
use guido::prelude::*;
fn main() {
container().shadow(Shadow::new((0.0, 4.0), 8.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.2)));

// `simple` leaves the spread at zero, which is what most shadows want.
container().shadow(Shadow::simple((0.0, 2.0), 4.0, Color::rgba(0.0, 0.0, 0.0, 0.16)));

// And `none` is a shadow that draws nothing — the resting value to animate out of.
container().shadow(Shadow::none())
;
}

The offset moves the shadow, and it moves in both axes: (12.0, 0.0) throws it to the right, (0.0, -6.0) throws it upward. Blur is the softness of the edge. Spread grows the shadow in every direction before it is blurred. The colour is a colour, alpha included — a shadow does not have to be black.

A shadow is drawn outside the box and never under it, as a browser draws a box-shadow. With an opaque background that makes no difference; with a translucent one it does: the box shows what is behind it, exactly as it would with no shadow, and a coloured glow lights up the box’s surroundings rather than tinting the box.

Inset shadows

.inset() casts the same shadow inside the box, as CSS’s inset keyword does: over the fill and inside the border, dark at the edge and fading inwards. The offset moves it as it moves an outer one, so (0.0, 3.0) darkens the top edge — a control pressed into the surface — and a spread pushes the edge further in. With no blur, a spread is a hard ring just inside the edge.

extern crate guido;
use guido::prelude::*;
fn main() {
// A glow inside the edge, for something that wants attention.
container().shadow(Shadow::new((0.0, 0.0), 12.0, 0.0, Color::rgba(1.0, 0.3, 0.2, 0.6)).inset());

// Pressed in.
container().shadow(Shadow::new((0.0, 3.0), 4.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.35)).inset());

// A ring, 1.5 px deep.
container().shadow(Shadow::new((0.0, 0.0), 0.0, 1.5, Color::WHITE).inset())
;
}

An outer shadow rides the box’s fill and draws nothing without one, since a shadow with no box above it is a smear. An inset shadow is inside the box, and an empty box can glow.

More than one shadow

.shadow(..) holds a Shadows: a list of up to four, which one Shadow or an array of them converts into. They are drawn as CSS draws a box-shadow list — the first on top — and outer and inset shadows mix in one list. Most design systems spell an elevation as two shadows, a tight one that draws the edge and a soft one that lifts the card:

extern crate guido;
use guido::prelude::*;
const RAISED: Shadows = Shadows::new([
    Shadow::new((0.0, 1.0), 2.0, 0.0, Color::rgba(0.0, 0.0, 0.0, 0.3)),
    Shadow::new((0.0, 4.0), 8.0, 3.0, Color::rgba(0.0, 0.0, 0.0, 0.15)),
]);

fn main() {
container().shadow(RAISED);

// An array is a list too, inset shadows included.
container().shadow([
    Shadow::simple((0.0, 2.0), 4.0, Color::rgba(0.0, 0.0, 0.0, 0.2)),
    Shadow::new((0.0, 0.0), 6.0, 0.0, Color::rgba(1.0, 1.0, 1.0, 0.5)).inset(),
])
;
}

Shadows::new is const and refuses more than Shadows::MAX at compile time. A list animates shadow by shadow: the shorter of two is padded with shadows that draw nothing, so a second shadow arriving on hover fades in.

There is no elevation level

guido ships no elevation and no table behind one. That is deliberate. A single number can only reach one of the four degrees of freedom, and the table that expands it has to choose the other three on your behalf: always straight down, always black, never any spread.

What a design system wants from elevation is coordination — that every card in an application agrees about how high it sits. That is a set of constants, and it belongs to the application. Shadow::new, Shadow::simple and Shadow::none are all const, so a ladder costs nothing:

extern crate guido;
mod elevation {
    use guido::prelude::{Color, Shadow};

    const fn step(offset_y: f32, blur: f32, alpha: f32) -> Shadow {
        Shadow::new((0.0, offset_y), blur, 0.0, Color::rgba(0.0, 0.0, 0.0, alpha))
    }

    pub const FLAT: Shadow = Shadow::none();
    pub const LOW: Shadow = step(1.0, 3.0, 0.12);
    pub const RAISED: Shadow = step(3.0, 6.0, 0.19);
    pub const HIGH: Shadow = step(6.0, 10.0, 0.22);
}
fn main() {}

Then container().shadow(elevation::RAISED) reads the way .elevation(3.0) used to, and the three shadows below it are still reachable.

Shadows in state layers

A shadow is a paint property like any other, so a state layer overrides it:

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));
const FLAT: Shadow = Shadow::none();
fn main() {
container()
    .shadow(LOW)
    .when_hovered(|s| s.shadow(RAISED))   // lift on hover
    .when_pressed(|s| s.shadow(FLAT))     // press it back down
;
}

Motion

The timing rides with the declaration, never with the override — the same rule every animatable property follows:

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.transition(Transition::new(200.0, TimingFunction::EaseOut)))
    .when_hovered(|s| s.shadow(RAISED))
;
}

All four fields interpolate, so a shadow can change colour as it grows, or slide from below a card to beside it. when_hovered(|s| s.shadow(RAISED.transition(..))) does not compile: a state layer supplies a value, and the declaration says how it moves.

Shadows and corners

The shadow follows the shape the container is drawn with, curvature included:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .corners(Corners::squircle(12.0))
    .shadow(Shadow::simple((0.0, 8.0), 16.0, Color::rgba(0.0, 0.0, 0.0, 0.22)))
;
}

A card, end to end

extern crate guido;
use guido::prelude::*;
mod elevation {
    use guido::prelude::{Color, Shadow};
    const fn step(offset_y: f32, blur: f32, alpha: f32) -> Shadow {
        Shadow::new((0.0, offset_y), blur, 0.0, Color::rgba(0.0, 0.0, 0.0, alpha))
    }
    pub const LOW: Shadow = step(1.0, 3.0, 0.12);
    pub const RAISED: Shadow = step(3.0, 6.0, 0.19);
    pub const HIGH: Shadow = step(6.0, 10.0, 0.22);
}
fn card(title: &str, body: &str) -> Container {
    container()
        .width(200.0)
        .padding(20.0)
        .background(Color::rgb(0.15, 0.15, 0.2).transition(150.0))
        .corners(12.0)
        .shadow(elevation::RAISED.transition(200.0))
        .when_hovered(|s| s.shadow(elevation::HIGH).lighter(0.05))
        .when_pressed(|s| s.shadow(elevation::LOW).darker(0.05))
        .layout(Flex::column().spacing(8.0))
        .children([
            container().child(text(title).font_size(18.0).color(Color::WHITE)),
            container().child(text(body).color(Color::rgb(0.7, 0.7, 0.75))),
        ])
}
fn main() {}

cargo run --example shadow_example shows a ladder beside the three things one number could never say: a shadow thrown sideways, a coloured one, and one with spread.

On dark backgrounds

A black shadow on a dark surface is nearly invisible. Two ways out, and they compose: lighten the surface as it rises, or give the shadow a colour.

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));
fn main() {
container()
    .background(Color::rgb(0.12, 0.12, 0.16))
    .shadow(LOW)
    .when_hovered(|s| {
        s.lighter(0.03)
            .shadow(Shadow::simple((0.0, 8.0), 20.0, Color::rgba(0.35, 0.4, 0.9, 0.45)))
    })
;
}