Shadows
Bidirectional shadow transformation — including generating shadows from scratch when dark UIs have none.
The Challenge
Shadows are the hardest part of bidirectional theme generation. In light mode, shadows provide depth and elevation cues. In dark mode, shadows are nearly invisible and elevation is communicated through surface lightness instead. Nightfall handles both directions correctly.
Light to Dark: Adapting Shadows
When transforming light to dark, existing shadows need to become more prominent because they have less contrast against dark backgrounds:
- Opacity increased by 3-5x — A shadow at
rgba(0,0,0,0.05)becomesrgba(0,0,0,0.30) - Blur radius increased by 1.2-1.5x — Slightly softer to compensate for the increased opacity
- Spread reduced slightly — Tighter shadows look more natural on dark surfaces
/* Original light shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07),;
0 2px 4px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08),;
0 4px 6px -4px rgba(0, 0, 0, 0.04);
/* Transformed dark shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.30);
--shadow-md: 0 4px 8px -1px rgba(0, 0, 0, 0.40),;
0 2px 5px -2px rgba(0, 0, 0, 0.30);
--shadow-lg: 0 10px 20px -3px rgba(0, 0, 0, 0.50),;
0 4px 8px -4px rgba(0, 0, 0, 0.25);Dark to Light: Generating from Scratch
Dark UIs often have zero shadows because they are invisible on dark backgrounds. When generating a light theme from a dark source, Nightfall creates a complete shadow scale from scratch based on the elevation model:
function generateShadowScale(elevationLevels) {
// Map each elevation level to a shadow
// Higher elevation = more prominent shadow
return {
xs: '0 1px 2px rgba(0, 0, 0, 0.04)',
sm: '0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04)',
md: '0 4px 6px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.04)',
lg: '0 10px 15px rgba(0, 0, 0, 0.06), 0 4px 6px rgba(0, 0, 0, 0.04)',
xl: '0 20px 25px rgba(0, 0, 0, 0.08), 0 8px 10px rgba(0, 0, 0, 0.04)',
'2xl': '0 25px 50px rgba(0, 0, 0, 0.14)',
};
}Generated Shadow Scale
The generated shadow scale follows established conventions from Material Design and Tailwind CSS, using layered shadows for natural depth:
/* Generated shadow scale for light theme */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06),;
0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05),;
0 2px 4px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.06),;
0 4px 6px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.08),;
0 8px 10px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px rgba(0, 0, 0, 0.14);Elevation-to-Shadow Mapping
Nightfall maps the dark theme's lightness-based elevation to shadow-based elevation in the light theme:
# Dark theme elevation (lightness-based) → Light theme elevation (shadow-based)
# --bg-page L=0.13 (base) → --bg-page white, no shadow
# --bg-surface L=0.17 (+0.04 from base) → --bg-surface white + shadow-sm
# --bg-elevated L=0.21 (+0.08 from base) → --bg-elevated white + shadow-md
# --bg-overlay L=0.25 (+0.12 from base) → --bg-overlay white + shadow-lgMulti-Layer Shadows
Each shadow level uses two layers: a larger ambient shadow and a smaller direct shadow. This produces a more natural, physically-inspired appearance compared to single-layer shadows:
/* Single layer (flat, unnatural) */
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.10);
/* Two layers (natural, depth) */
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05), /* ambient */;
0 2px 4px rgba(0, 0, 0, 0.04); /* direct */