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

Live Demo: Outer Shadow

Open the demo in a separate tab.

Live Demo: Inner Shadow

Open the demo in a separate tab.

Beverly brand mark

Shadows help show elevation and surface separation. They are best when subtle and consistent, and they should support the interface without becoming decoration. In a well-designed system, shadows are not a visual gimmick; they are a way to communicate depth, layering, and separation in a controlled, readable way.

What shadows communicate

A shadow is one of the clearest ways to tell a user that one surface sits above another. It helps the eye understand:

  • which panel is floating
  • which control is active or elevated
  • which content is layered above the background
  • which overlay needs stronger separation from the page beneath it

This makes shadows a useful tool for hierarchy, especially in dense or highly structured interfaces.

Drop shadow sizes

Beverly keeps a small set of shadow presets so surfaces can communicate depth in a predictable way.

Small

Use a small shadow for light lift: compact cards, hover states, quick actions, and other subtle surfaces that should feel slightly separated from the background.

Regular

Use the regular shadow for the default elevated surface. This is the standard choice for cards, property panels, and other content that needs a clear separation but should still feel understated.

Large

Use the large shadow for major floating overlays: modals, drawers, menus, and panels that need a strong sense of separation from the page beneath them.

Example

#![allow(unused)]
fn main() {
use beverly::rendering::{OuterShadow, Paint, Surface};
use bevy::prelude::*;

fn elevated_surface(parent: &mut ChildSpawnerCommands) {
    parent.spawn((
        Node {
            width: Val::Px(240.0),
            height: Val::Px(120.0),
            ..default()
        },
        Surface::rounded_rect_fill(8.0, Paint::solid(Color::WHITE))
            .outer_shadow(OuterShadow::regular(Color::BLACK)),
    ));
}
}

BeverlyPlugin installs the shader-backed surface renderer. Surface::outer_shadow adds a drop shadow outside the surface’s shape without changing its layout dimensions.

Preset Values

Each preset accepts a shadow color and uses smooth falloff with zero spread:

ConstructorOffset X / YBlurOpacity
OuterShadow::small(color)0 / 2 px4 px0.075
OuterShadow::regular(color)0 / 8 px16 px0.15
OuterShadow::large(color)0 / 16 px48 px0.175

These are Bootstrap-style presets. They are a convenient starting point, not a requirement to use the same shadow on every surface.

Custom Drop Shadow

#![allow(unused)]
fn main() {
use beverly::rendering::{OuterShadow, Paint, ShadowFalloff, Surface};
use bevy::prelude::*;

let shadow = OuterShadow::new(Color::BLACK)
    .with_offset(Vec2::new(0.0, 12.0))
    .with_blur(24.0)
    .with_spread(0.0)
    .with_opacity(0.35)
    .with_falloff(ShadowFalloff::Smooth);

let surface = Surface::rounded_rect_fill(8.0, Paint::solid(Color::WHITE))
    .outer_shadow(shadow);
}
ParameterEffect
colorShadow tint; black is the usual neutral choice
offsetLogical pixel displacement; positive X moves right, positive Y moves down
blurEdge softness in pixels; larger values produce a broader soft edge
spreadExpands the shadow silhouette when positive, contracts it when negative
opacityShadow strength from 0.0 to 1.0
falloffHow the shadow fades away from its silhouette

OuterShadow::new(color) starts with zero opacity, so set .with_opacity(...) to make a custom shadow visible. Blur and falloff are shader parameters, not a CSS box-shadow declaration; compare the rendered result rather than expecting pixel-identical browser output.

Falloff

  • ShadowFalloff::Linear: a linear fade.
  • ShadowFalloff::Smooth: a smooth fade, used by the presets.
  • ShadowFalloff::Gaussian: a Gaussian-style fade.

Falloff changes the fade profile, independently of the shadow’s offset and spread.

Runtime Updates

Change surface.effects.outer_shadow to update a shadow at runtime:

#![allow(unused)]
fn main() {
use beverly::rendering::{OuterShadow, Surface};
use bevy::prelude::*;

fn set_shadow_strength(surface: &mut Surface, opacity: f32) {
    surface.effects.outer_shadow = Some(
        OuterShadow::regular(Color::BLACK)
            .with_opacity(opacity.clamp(0.0, 1.0)),
    );
}
}

Use surface.clear_outer_shadow() to remove it. A surface has one outer-shadow slot; setting another shadow replaces the previous one.

Leave space around the surface for the shadow to render. A clipping ancestor can cut off the effect, and shadows do not reserve space between neighboring elements automatically.

Explicit shadow colors do not change automatically with the theme. Black shadows are less visible on dark backgrounds; use an appropriate surface fill and a subtle border to maintain separation rather than relying on the shadow alone.

Demo

Run the interactive drop-shadow demo from the repository root:

cargo run --example outer_shadow
cargo run --example outer_shadow -- dark

The demo includes horizontal and vertical offset sliders, softness, spread, and opacity controls, plus Linear, Smooth, and Gaussian falloff selectors. The main preview uses the selected parameters; the Small, Regular, and Large samples retain their preset shadows for comparison.

The top-right toggle switches light/dark mode. Surface fills and borders follow the theme while the shadows remain black.

Inner Shadows

InnerShadow paints shading inside a surface’s shape, making it appear recessed rather than elevated. Use it for inset trays, pressed surfaces, and recessed controls. It is a shader-backed surface effect, not a separate child node or a background-blur operation.

#![allow(unused)]
fn main() {
use beverly::rendering::{InnerShadow, Paint, ShadowFalloff, Surface};
use bevy::prelude::*;

fn add_recessed_tray(parent: &mut ChildSpawnerCommands) {
    parent.spawn((
        Node {
            width: Val::Px(300.0),
            height: Val::Px(180.0),
            padding: UiRect::all(Val::Px(40.0)),
            ..default()
        },
        Surface::rounded_rect_fill(8.0, Paint::solid(Color::WHITE))
            .inner_shadow(
                InnerShadow::new(Color::BLACK)
                    .with_offset(Vec2::new(0.0, 8.0))
                    .with_blur(16.0)
                    .with_spread(4.0)
                    .with_opacity(0.45)
                    .with_falloff(ShadowFalloff::Smooth),
            ),
    ));
}
}

BeverlyPlugin includes the renderer required for this example. The shading follows the surface’s rounded corners; it does not change the node’s size or reserve padding for its contents.

Inset Parameters

The inner-shadow API uses the same parameter names as OuterShadow:

SetterPurpose
with_offset(Vec2)Displace the shadow in logical UI pixels; X points right and Y points down
with_blur(pixels)Adjust the softness of the inset shading
with_spread(pixels)Adjust the inset shadow’s spread; signed values are supported
with_opacity(value)Set strength from 0.0 to 1.0
with_falloff(mode)Choose Linear, Smooth, or Gaussian fade

The constructor’s color sets the shadow tint. InnerShadow::new(color) defaults to zero opacity, so set an opacity explicitly or use a preset. Compare inset spread visually rather than assuming it expands like an outer shadow: the shading is constrained to the inside of the shape.

Inset Presets

All three presets use zero spread and smooth falloff:

ConstructorOffset X / YBlurOpacity
InnerShadow::small(color)0 / 1 px2 px0.075
InnerShadow::regular(color)0 / 2 px4 px0.125
InnerShadow::large(color)0 / 4 px8 px0.175
#![allow(unused)]
fn main() {
use beverly::rendering::{InnerShadow, Paint, Surface};
use bevy::prelude::*;

let surface = Surface::rounded_rect_fill(8.0, Paint::solid(Color::WHITE))
    .inner_shadow(InnerShadow::regular(Color::BLACK));
}

Updating and Combining

Update surface.effects.inner_shadow at runtime, or call surface.clear_inner_shadow() to remove it. Each surface has one inner-shadow slot; a new value replaces the existing inset shadow.

Inner and outer shadows occupy separate slots, so a surface can use both. Borders and fills remain independently configurable. Explicit shadow colors are not automatically replaced when the theme changes.

Keep text clear of heavy edge shading with layout padding. In dark mode, black inset shading can become subtle; preserve readable surface contrast and do not use the shadow as the only indication of focus, selection, or disabled state.

Inner Shadow Demo

Run the interactive inset-shadow demo from the repository root:

cargo run --example inner_shadow
cargo run --example inner_shadow -- dark

The demo includes horizontal and vertical offsets, softness, spread, and opacity controls, plus three falloff selectors. The main recessed tray follows these settings while Small, Regular, and Large samples retain their preset shadows.

The top-right toggle switches light/dark mode. Surface fills and borders follow the theme while the shadows remain black.

Common uses

  • cards and panels
  • modal shells and overlays
  • hover states on floating controls
  • tooltips and context menus
  • data surfaces that need a clear sense of separation from the background

Good shadow behavior

A strong shadow system should be:

  • consistent across the app
  • tuned to the actual elevation scale
  • subtle enough to preserve readability
  • distinct enough to communicate hierarchy
  • easy to adjust for dark and light themes

The goal is not to create heavy, dramatic shadows everywhere. It is to establish a clear system of depth that feels calm and intentional.

Avoiding over-shadowing

Too many strong shadows make the interface feel noisy and visually crowded. If every card and panel has a large visible shadow, the hierarchy becomes muddy and the app loses calmness.

This is why Beverly should favor a small set of preset values rather than a free-form shadow system that encourages each component to tune itself independently. A restrained preset model makes the product feel coherent.

Shadows and accessibility

Shadows are not a replacement for contrast or focus states. They should support a surface, not hide a problem. In a high-contrast or reduced-visual-noise environment, a strong shadow may become too heavy or unnecessary.

That means shadow intensity should be part of the design policy layer, not just a static decoration value. The system should be able to reduce or suppress certain effects when accessibility or theme constraints require it.

The principle

Use a single shadow system with a small set of presets so the app stays coherent across components. Pick the lightest shadow that still communicates the right depth. The best shadows are the ones users do not consciously notice because they make the structure clearer without interrupting the content.