Design Tokens

Box Shadows

Create and manage box shadow design tokens with CSS variables for consistent elevation and depth effects across your application.

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;

The default values include:

  • none: No shadow
  • xs: 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 states
  • ring: Subtle ring that maintains elevation feel for focus states
Good to know: The default shadows use 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;

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;

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., smboxShadowSm, 2xlboxShadow2xl).

Pro tip: Use layered shadows (multiple shadow values) for more realistic depth. Combining a sharp close shadow with a softer distant shadow creates natural-looking elevation.

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;
Pro tip: The --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;

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;
Good to know: In dark themes, you may want to use lighter, more subtle shadows or even inverted shadows (white) with very low opacity. This helps maintain visual hierarchy without overpowering the dark background.

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.
Pro tip: For the most natural-looking shadows, use two layers: one tight shadow (1-2px blur) and one diffuse shadow (larger blur with negative spread). This mimics how real-world shadows work with both sharp and soft components.

FAQ