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)))
})
;
}