Box Shadows
Overview
The box shadow composable helps you create comprehensive shadow systems with minimal code. It generates box-shadow variables that can be easily referenced throughout your application, enabling flexible theming and consistent visual hierarchy for your components through elevation and depth.
Why use box shadow composables?
Box shadow composables help you:
- Establish visual hierarchy: Create consistent elevation systems that help users understand interface depth and layering.
- Enable flexible theming: Override shadow variables to instantly update component shadows across your application.
- Dynamic shadow colors: Use a single color variable to control shadow colors throughout your entire shadow system.
- Maintain consistency: Use semantic names to ensure consistent shadow usage throughout your design system.
- Reduce repetition: Reference shadow variables instead of repeating complex CSS values throughout your stylesheets.
- Simplify elevation systems: Define your entire elevation scale in one place for easy management and updates.
useBoxShadow
Styleframe provides a set of carefully crafted default shadow values that you can use out of the box:
import { styleframe } from 'styleframe';
import { useBoxShadow } from '@styleframe/theme';
const s = styleframe();
const {
boxShadow,
boxShadowNone,
boxShadowXs,
boxShadowSm,
boxShadowMd,
boxShadowLg,
boxShadowXl,
boxShadow2xl,
boxShadowInner,
boxShadowRing,
} = useBoxShadow(s);
export default s;
:root {
--box-shadow--none: none;
--box-shadow--xs: 0 1px 1px oklch(var(--box-shadow-color, 0 0 0) / 0.12), 0 2px 2px -1px oklch(var(--box-shadow-color, 0 0 0) / 0.06);
--box-shadow--sm: 0 1px 2px oklch(var(--box-shadow-color, 0 0 0) / 0.14), 0 3px 6px -1px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--md: 0 2px 4px oklch(var(--box-shadow-color, 0 0 0) / 0.16), 0 8px 16px -4px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--lg: 0 4px 8px oklch(var(--box-shadow-color, 0 0 0) / 0.18), 0 16px 24px -8px oklch(var(--box-shadow-color, 0 0 0) / 0.12);
--box-shadow--xl: 0 8px 12px oklch(var(--box-shadow-color, 0 0 0) / 0.20), 0 24px 48px -12px oklch(var(--box-shadow-color, 0 0 0) / 0.14);
--box-shadow--2xl: 0 12px 16px oklch(var(--box-shadow-color, 0 0 0) / 0.22), 0 32px 64px -16px oklch(var(--box-shadow-color, 0 0 0) / 0.16);
--box-shadow--inner: inset 0 1px 0 oklch(var(--box-shadow-color, 0 0 0) / 0.08), inset 0 0 0 1px oklch(var(--box-shadow-color, 0 0 0) / 0.06);
--box-shadow--ring: 0 0 0 1px oklch(var(--box-shadow-color, 0 0 0) / 0.12), 0 1px 2px oklch(var(--box-shadow-color, 0 0 0) / 0.08);
--box-shadow: var(--box-shadow--md);
}
The default values include:
none
: No shadowxs
: Subtle shadow for cards and surfaces (1-2px offset)sm
: Standard shadow for most elevated elements (1-6px offset)md
(default): Medium shadow for popovers and raised buttons (2-16px offset)lg
: Large shadow for modals and floating panels (4-24px offset)xl
: Extra large shadow for drawers and high elevation (8-48px offset)2xl
: Maximum shadow for toasts over content (12-64px offset)inner
: Inset shadow for wells and pressed statesring
: Subtle ring that maintains elevation feel for focus states
oklch(var(--box-shadow-color, 0 0 0) / opacity)
syntax, which allows you to control the shadow color globally by setting a --box-shadow-color
variable. The fallback is black (0 0 0
in OKLCH format).Extending the Default Box Shadow Values
You can customize which box shadow is used as the default while keeping all other standard styles. Use the @
prefix to reference another key in the values object:
import { styleframe } from 'styleframe';
import { useBoxShadow, defaultBoxShadowValues } from '@styleframe/theme';
const s = styleframe();
const { boxShadow } = useBoxShadow(s, {
...defaultBoxShadowValues,
default: '@lg'
});
export default s;
:root {
--box-shadow--none: none;
--box-shadow--xs: 0 1px 1px oklch(var(--box-shadow-color, 0 0 0) / 0.12), 0 2px 2px -1px oklch(var(--box-shadow-color, 0 0 0) / 0.06);
--box-shadow--sm: 0 1px 2px oklch(var(--box-shadow-color, 0 0 0) / 0.14), 0 3px 6px -1px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--md: 0 2px 4px oklch(var(--box-shadow-color, 0 0 0) / 0.16), 0 8px 16px -4px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--lg: 0 4px 8px oklch(var(--box-shadow-color, 0 0 0) / 0.18), 0 16px 24px -8px oklch(var(--box-shadow-color, 0 0 0) / 0.12);
--box-shadow--xl: 0 8px 12px oklch(var(--box-shadow-color, 0 0 0) / 0.20), 0 24px 48px -12px oklch(var(--box-shadow-color, 0 0 0) / 0.14);
--box-shadow--2xl: 0 12px 16px oklch(var(--box-shadow-color, 0 0 0) / 0.22), 0 32px 64px -16px oklch(var(--box-shadow-color, 0 0 0) / 0.16);
--box-shadow--inner: inset 0 1px 0 oklch(var(--box-shadow-color, 0 0 0) / 0.08), inset 0 0 0 1px oklch(var(--box-shadow-color, 0 0 0) / 0.06);
--box-shadow--ring: 0 0 0 1px oklch(var(--box-shadow-color, 0 0 0) / 0.12), 0 1px 2px oklch(var(--box-shadow-color, 0 0 0) / 0.08);
--box-shadow: var(--box-shadow--lg);
}
Creating Custom Box Shadow Variables
The useBoxShadow()
function creates a set of box shadow variables for establishing visual elevation and depth in your interface.
import { styleframe } from 'styleframe';
import { useBoxShadow } from '@styleframe/theme';
const s = styleframe();
const {
boxShadow,
boxShadowNone,
boxShadowSm,
boxShadowMd,
boxShadowLg,
} = useBoxShadow(s, {
default: '@md',
none: 'none',
sm: '0 1px 1px oklch(0 0 0 / 0.12), 0 2px 2px -1px oklch(0 0 0 / 0.06)',
md: '0 2px 4px oklch(0 0 0 / 0.16), 0 8px 16px -4px oklch(0 0 0 / 0.10)',
lg: '0 4px 8px oklch(0 0 0 / 0.18), 0 16px 24px -8px oklch(0 0 0 / 0.12)',
});
export default s;
:root {
--box-shadow--none: none;
--box-shadow--sm: 0 1px 1px oklch(0 0 0 / 0.12), 0 2px 2px -1px oklch(0 0 0 / 0.06);
--box-shadow--md: 0 2px 4px oklch(0 0 0 / 0.16), 0 8px 16px -4px oklch(0 0 0 / 0.10);
--box-shadow--lg: 0 4px 8px oklch(0 0 0 / 0.18), 0 16px 24px -8px oklch(0 0 0 / 0.12);
--box-shadow: var(--box-shadow--md);
}
The function creates variables for each shadow level you define. Each key in the object becomes a box shadow variable with the prefix box-shadow--
, and the export name is automatically converted to camelCase (e.g., sm
→ boxShadowSm
, 2xl
→ boxShadow2xl
).
Using Box Shadow Color Variables
One of the most powerful features of the default shadow system is the ability to control shadow colors dynamically:
import { styleframe } from 'styleframe';
import { useBoxShadow, defaultBoxShadowValues } from '@styleframe/theme';
const s = styleframe();
const { ref, selector, css } = s;
// Define a shadow color variable (OKLCH format: lightness chroma hue)
const boxShadowColor = s.variable('box-shadow-color', '0 0 0');
// Use default shadows (which reference --box-shadow-color)
const { boxShadow, boxShadowMd, boxShadowLg } = useBoxShadow(
s,
defaultBoxShadowValues
);
// Apply shadows to components
selector('.card', {
boxShadow: ref(boxShadow),
});
selector('.modal', {
boxShadow: ref(boxShadowLg),
});
// Override shadow color for specific contexts
selector('.card-primary', (ctx) => {
ctx.variable(boxShadowColor, '0.6109 0.1903 263.71'); // Blue shadows in OKLCH
return {
boxShadow: ref(boxShadowMd),
}
});
export default s;
:root {
--box-shadow-color: 0 0 0;
--box-shadow--none: none;
--box-shadow--xs: 0 1px 1px oklch(var(--box-shadow-color, 0 0 0) / 0.12), 0 2px 2px -1px oklch(var(--box-shadow-color, 0 0 0) / 0.06);
--box-shadow--sm: 0 1px 2px oklch(var(--box-shadow-color, 0 0 0) / 0.14), 0 3px 6px -1px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--md: 0 2px 4px oklch(var(--box-shadow-color, 0 0 0) / 0.16), 0 8px 16px -4px oklch(var(--box-shadow-color, 0 0 0) / 0.10);
--box-shadow--lg: 0 4px 8px oklch(var(--box-shadow-color, 0 0 0) / 0.18), 0 16px 24px -8px oklch(var(--box-shadow-color, 0 0 0) / 0.12);
--box-shadow: var(--box-shadow--md);
}
.card {
box-shadow: var(--box-shadow);
}
.modal {
box-shadow: var(--box-shadow--lg);
}
.card-primary {
--box-shadow-color: 0.6109 0.1903 263.71;
box-shadow: var(--box-shadow--md);
}
--box-shadow-color
variable expects OKLCH values without the oklch()
wrapper (e.g., 0 0 0
for lightness, chroma, and hue). This format works with the modern CSS color syntax used in the shadow definitions.Examples
Custom Shadow System
Here's how to create a complete shadow system with semantic naming for different UI components:
import { styleframe } from 'styleframe';
import { useBoxShadow } from '@styleframe/theme';
const s = styleframe();
const { ref, selector, css } = s;
// Define shadow color (OKLCH format: lightness chroma hue)
const boxShadowColor = s.variable('box-shadow-color', '0 0 0');
// Create comprehensive shadow system
const {
boxShadowNone,
boxShadowCard,
boxShadowButton,
boxShadowDropdown,
boxShadowModal,
boxShadowDrawer,
boxShadowToast,
boxShadowFocus,
boxShadowInset,
} = useBoxShadow(s, {
none: 'none',
card: css`0 1px 3px oklch(${ ref(boxShadowColor) } / 0.12), 0 1px 2px oklch(${ ref(boxShadowColor) } / 0.06)`,
button: css`0 1px 2px oklch(${ ref(boxShadowColor) } / 0.14), 0 2px 4px oklch(${ ref(boxShadowColor) } / 0.10)`,
dropdown: css`0 4px 6px oklch(${ ref(boxShadowColor) } / 0.16), 0 10px 20px -4px oklch(${ ref(boxShadowColor) } / 0.10)`,
modal: css`0 8px 16px oklch(${ ref(boxShadowColor) } / 0.18), 0 20px 40px -8px oklch(${ ref(boxShadowColor) } / 0.12)`,
drawer: css`0 12px 24px oklch(${ ref(boxShadowColor) } / 0.20), 0 30px 60px -12px oklch(${ ref(boxShadowColor) } / 0.14)`,
toast: css`0 16px 32px oklch(${ ref(boxShadowColor) } / 0.22), 0 40px 80px -16px oklch(${ ref(boxShadowColor) } / 0.16)`,
focus: css`0 0 0 3px oklch(0.6109 0.1903 263.71 / 0.3)`,
inset: css`inset 0 2px 4px oklch(${ ref(boxShadowColor) } / 0.08)`,
});
// Apply to components
selector('.card', {
boxShadow: ref(boxShadowCard),
transition: 'box-shadow 0.2s ease',
'&:hover': {
boxShadow: ref(boxShadowButton),
},
});
selector('.btn', {
boxShadow: ref(boxShadowButton),
'&:active': {
boxShadow: ref(boxShadowNone),
transform: 'translateY(1px)',
},
'&:focus-visible': {
boxShadow: css`${ref(boxShadowButton)}, ${ref(boxShadowFocus)}`,
},
});
selector('.dropdown', {
boxShadow: ref(boxShadowDropdown),
});
selector('.modal', {
boxShadow: ref(boxShadowModal),
});
selector('.drawer', {
boxShadow: ref(boxShadowDrawer),
});
selector('.toast', {
boxShadow: ref(boxShadowToast),
});
selector('.input', {
'&:focus': {
boxShadow: ref(boxShadowFocus),
},
});
selector('.well', {
boxShadow: ref(boxShadowInset),
});
export default s;
:root {
--box-shadow-color: 0 0 0;
--box-shadow--none: none;
--box-shadow--card: 0 1px 3px oklch(var(--box-shadow-color) / 0.12), 0 1px 2px oklch(var(--box-shadow-color) / 0.06);
--box-shadow--button: 0 1px 2px oklch(var(--box-shadow-color) / 0.14), 0 2px 4px oklch(var(--box-shadow-color) / 0.10);
--box-shadow--dropdown: 0 4px 6px oklch(var(--box-shadow-color) / 0.16), 0 10px 20px -4px oklch(var(--box-shadow-color) / 0.10);
--box-shadow--modal: 0 8px 16px oklch(var(--box-shadow-color) / 0.18), 0 20px 40px -8px oklch(var(--box-shadow-color) / 0.12);
--box-shadow--drawer: 0 12px 24px oklch(var(--box-shadow-color) / 0.20), 0 30px 60px -12px oklch(var(--box-shadow-color) / 0.14);
--box-shadow--toast: 0 16px 32px oklch(var(--box-shadow-color) / 0.22), 0 40px 80px -16px oklch(var(--box-shadow-color) / 0.16);
--box-shadow--focus: 0 0 0 3px oklch(0.6109 0.1903 263.71 / 0.3);
--box-shadow--inset: inset 0 2px 4px oklch(var(--box-shadow-color) / 0.08);
}
.card {
box-shadow: var(--box-shadow--card);
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: var(--box-shadow--button);
}
}
.btn {
box-shadow: var(--box-shadow--button);
&:active {
box-shadow: var(--box-shadow--none);
transform: translateY(1px);
}
&:focus-visible {
box-shadow: var(--box-shadow--button), var(--box-shadow--focus);
}
}
.dropdown {
box-shadow: var(--box-shadow--dropdown);
}
.modal {
box-shadow: var(--box-shadow--modal);
}
.drawer {
box-shadow: var(--box-shadow--drawer);
}
.toast {
box-shadow: var(--box-shadow--toast);
}
.input:focus {
box-shadow: var(--box-shadow--focus);
}
.well {
box-shadow: var(--box-shadow--inset);
}
Theme-Aware Shadows
Create shadows that automatically adapt to light and dark themes by adjusting shadow opacity and color:
import { styleframe } from 'styleframe';
import { useBoxShadow, defaultBoxShadowValues } from '@styleframe/theme';
const s = styleframe();
const { ref, selector, theme } = s;
// Define shadow color (OKLCH format: lightness chroma hue)
const boxShadowColor = s.variable('box-shadow-color', '0 0 0');
// Use default shadows (which reference --box-shadow-color)
const { boxShadow, boxShadowMd, boxShadowLg } = useBoxShadow(
s,
defaultBoxShadowValues
);
// Override shadow color in dark theme
theme('dark', (ctx) => {
// Use lighter shadows in dark mode for better contrast
ctx.variable(boxShadowColor, '1 0 0');
});
// Apply shadows
selector('.card', {
boxShadow: ref(boxShadow),
});
selector('.modal', {
boxShadow: ref(boxShadowLg),
});
export default s;
:root {
--box-shadow-color: 0 0 0;
--box-shadow--none: none;
--box-shadow--xs: 0 1px 1px oklch(var(--box-shadow-color) / 0.12), 0 2px 2px -1px oklch(var(--box-shadow-color) / 0.06);
--box-shadow--sm: 0 1px 2px oklch(var(--box-shadow-color) / 0.14), 0 3px 6px -1px oklch(var(--box-shadow-color) / 0.10);
--box-shadow--md: 0 2px 4px oklch(var(--box-shadow-color) / 0.16), 0 8px 16px -4px oklch(var(--box-shadow-color) / 0.10);
--box-shadow--lg: 0 4px 8px oklch(var(--box-shadow-color) / 0.18), 0 16px 24px -8px oklch(var(--box-shadow-color) / 0.12);
--box-shadow: var(--box-shadow--md);
}
[data-theme="dark"] {
--box-shadow-color: 1 0 96;
}
.card {
box-shadow: var(--box-shadow);
}
.modal {
box-shadow: var(--box-shadow--lg);
}
Custom Shadow Colors
Create colored shadows that match your brand colors for special components:
import { styleframe } from 'styleframe';
import { useColor, useBoxShadow } from '@styleframe/theme';
const s = styleframe();
const { ref, selector, css } = s;
// Define brand colors
const { colorPrimary, colorSuccess, colorWarning, colorDanger } = useColor(s, {
primary: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
});
// Create colored shadows
const {
boxShadowPrimary,
boxShadowSuccess,
boxShadowWarning,
boxShadowDanger,
} = useBoxShadow(s, {
primary: css`0 4px 8px oklch(from ${ref(colorPrimary)} l c h / 0.3), 0 2px 4px oklch(0.6109 0.1903 263.71 / 0.2)`,
success: css`0 4px 8px oklch(from ${ref(colorSuccess)} l c h / 0.3), 0 2px 4px oklch(0.7051 0.1654 165.47 / 0.2)`,
warning: css`0 4px 8px oklch(from ${ref(colorWarning)} l c h / 0.3), 0 2px 4px oklch(0.7768 0.1504 75.49 / 0.2)`,
danger: css`0 4px 8px oklch(from ${ref(colorDanger)} l c h / 0.3), 0 2px 4px oklch(0.6278 0.2158 27.33 / 0.2)`,
});
// Apply colored shadows
selector('.btn-primary', {
backgroundColor: ref(colorPrimary),
boxShadow: ref(boxShadowPrimary),
'&:hover': {
boxShadow: css`0 8px 16px oklch(from ${ref(colorPrimary)} l c h / 0.4), 0 4px 8px oklch(from ${ref(colorPrimary)} l c h / 0.25)`,
},
});
selector('.btn-success', {
backgroundColor: ref(colorSuccess),
boxShadow: ref(boxShadowSuccess),
});
selector('.btn-warning', {
backgroundColor: ref(colorWarning),
boxShadow: ref(boxShadowWarning),
});
selector('.btn-danger', {
backgroundColor: ref(colorDanger),
boxShadow: ref(boxShadowDanger),
});
export default s;
Best Practices
- Establish a clear hierarchy: Use progressively larger shadows to indicate higher elevation. Don't skip levels arbitrarily.
- Use layered shadows: Combine multiple shadow layers (a sharp close shadow + soft distant shadow) for more realistic depth.
- Keep opacity consistent: Within your shadow scale, maintain consistent opacity ratios between layers for visual harmony.
- Consider performance: Shadows can be expensive to render. Use
will-change: box-shadow
sparingly and only for animated shadows. - Don't overuse large shadows: Reserve the largest shadows (xl, 2xl) for only the highest elevation elements like modals and toasts.
- Use the shadow color variable: Leverage a color variable for dynamic theming rather than creating completely separate shadow scales.
- Test in dark mode: Shadows that look great in light mode may need adjustment for dark themes. Consider lighter, more subtle shadows.
- Combine with z-index: Elevation and z-index should work together. Higher shadows should correspond to higher z-index values.
- Use inset shadows sparingly: Inset shadows are great for pressed states and wells, but can look dated if overused.
FAQ
useBoxShadow()
centralizes your shadow system, making it easy to maintain consistency and update shadows globally. It also enables powerful features like dynamic shadow colors through --box-shadow-color
, theme-aware shadows, and semantic naming that makes your intent clear.Each shadow size represents a different elevation level:
- sm: Subtle cards and surfaces (1-2px)
- default: Standard elevated elements (1-6px)
- md: Popovers and raised buttons (2-16px)
- lg: Modals and floating panels (4-24px)
- xl: Drawers and high elevation (8-48px)
- 2xl: Toasts and maximum elevation (12-64px)
The size progression ensures clear visual hierarchy in your interface.
In dark mode, traditional black shadows can be too harsh or invisible. You have three approaches:
- Use a color variable (such as
--box-shadow-color
) to switch to lighter shadows (very subtle white shadows) - Reduce shadow opacity in dark mode while keeping black shadows
- Use lighter backgrounds for elevated elements instead of relying solely on shadows
Many designs combine approaches 2 and 3 for best results.
Most design systems work well with 5-8 shadow levels:
- 1 level for flat/none
- 3-4 levels for standard elevation (subtle → medium)
- 2-3 levels for high elevation (modals → toasts)
- Optional: specialized shadows (inset, focus rings)
Too many levels create inconsistency; too few limit flexibility. Start with the defaults and adjust based on your needs.
Borders
Create and manage border design tokens with CSS variables for consistent border styles, widths, and colors across your application.
Breakpoints
Create and manage responsive breakpoint design tokens with CSS variables for consistent media query handling and adaptive layouts across your application.