CSS Custom Properties and Theming

1. CSS Variable Declaration and Usage

Syntax Description Example Notes
--variable-name Declare custom property (must start with --) --primary-color: #007acc; Case-sensitive, use kebab-case
var(--name) Use custom property value color: var(--primary-color); Returns value of custom property
var(--name, fallback) Use with fallback value color: var(--color, blue); Uses fallback if variable undefined
:root Global scope (html element) :root { --spacing: 1rem; } Available to entire document
Inheritance Custom properties inherit by default Child elements inherit parent values Can be overridden at any level
Invalid value Invalid values make property invalid margin: var(--invalid); = invalid Falls back to inherited or initial value
Computed value Variables resolve at computed-value time Can use in calc(), color functions More flexible than preprocessor variables

Example: Basic CSS variable declaration and usage

/* Global variables in :root */
:root {
    --primary-color: #007acc;
    --secondary-color: #5c2d91;
    --text-color: #333;
    --bg-color: #fff;
    --spacing-unit: 8px;
    --border-radius: 4px;
    --font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}

/* Using variables */
.button {
    background: var(--primary-color);
    color: var(--bg-color);
    padding: calc(var(--spacing-unit) * 2);
    border-radius: var(--border-radius);
    font-family: var(--font-family);
}

/* With fallback values */
.card {
    background: var(--card-bg, #f9f9f9);
    border: 1px solid var(--card-border, #ddd);
    padding: var(--card-padding, 1rem);
}

/* Variables in calculations */
.container {
    padding: calc(var(--spacing-unit) * 3);
    margin-bottom: calc(var(--spacing-unit) * 4);
    max-width: calc(1200px - var(--spacing-unit) * 4);
}

/* Nested fallbacks */
.element {
    color: var(--text-color, var(--fallback-color, #000));
}

/* Using variables in other variables */
:root {
    --base-size: 16px;
    --small-size: calc(var(--base-size) * 0.875);
    --large-size: calc(var(--base-size) * 1.5);
    
    --primary: #007acc;
    --primary-dark: color-mix(in oklch, var(--primary) 80%, black);
    --primary-light: color-mix(in oklch, var(--primary) 90%, white);
}

Example: Component-scoped variables

/* Component with default variables */
.card {
    /* Local variables for this component */
    --card-bg: white;
    --card-padding: 1.5rem;
    --card-border: #e0e0e0;
    --card-shadow: 0 2px 4px rgba(0,0,0,0.1);
    
    background: var(--card-bg);
    padding: var(--card-padding);
    border: 1px solid var(--card-border);
    box-shadow: var(--card-shadow);
    border-radius: var(--border-radius, 8px);
}

/* Override for specific card types */
.card-featured {
    --card-bg: #f0f7ff;
    --card-border: #007acc;
    --card-shadow: 0 4px 8px rgba(0,122,204,0.2);
}

.card-warning {
    --card-bg: #fff3cd;
    --card-border: #ffc107;
}

/* Button component with variants */
.button {
    --btn-bg: var(--primary-color);
    --btn-color: white;
    --btn-padding: 0.75rem 1.5rem;
    --btn-hover-bg: var(--primary-dark);
    
    background: var(--btn-bg);
    color: var(--btn-color);
    padding: var(--btn-padding);
    border: none;
    border-radius: var(--border-radius);
    transition: background 0.2s;
}

.button:hover {
    background: var(--btn-hover-bg);
}

.button-secondary {
    --btn-bg: var(--secondary-color);
    --btn-hover-bg: var(--secondary-dark);
}

.button-outline {
    --btn-bg: transparent;
    --btn-color: var(--primary-color);
    border: 2px solid var(--primary-color);
}

.button-large {
    --btn-padding: 1rem 2rem;
}

/* Grid system with variables */
.grid {
    --grid-columns: 12;
    --grid-gap: 1rem;
    --grid-max-width: 1200px;
    
    display: grid;
    grid-template-columns: repeat(var(--grid-columns), 1fr);
    gap: var(--grid-gap);
    max-width: var(--grid-max-width);
    margin: 0 auto;
}

.grid-item {
    --span: 1;
    grid-column: span var(--span);
}

.grid-item-6 {
    --span: 6;
}

.grid-item-4 {
    --span: 4;
}
Note: CSS variables are case-sensitive. Use :root for global scope. Variables inherit, allowing component-level overrides. Use calc() for mathematical operations with variables. Browser support: all modern browsers (IE11 needs fallbacks).

2. Dynamic Theming Patterns

Pattern Technique Use Case Example
Data Attributes Set theme via HTML data attribute User-selectable themes <html data-theme="dark">
Class-based Apply theme class to root element Simple theme switching <body class="theme-blue">
CSS Variables Update variable values for themes Dynamic color systems --color: light-dark(#000, #fff)
JavaScript Updates Modify variables via JS Real-time theme changes element.style.setProperty('--color', value)
Inline Overrides Component-level theme overrides Contextual theming style="--bg: blue"
CSS @property Typed custom properties Animatable theme values Define syntax and initial value

Example: Multi-theme system with data attributes

/* Default theme (light) */
:root {
    --bg-primary: #ffffff;
    --bg-secondary: #f5f5f5;
    --text-primary: #1a1a1a;
    --text-secondary: #666666;
    --border-color: #e0e0e0;
    --accent-color: #007acc;
    --shadow: 0 2px 8px rgba(0,0,0,0.1);
}

/* Dark theme */
[data-theme="dark"] {
    --bg-primary: #1a1a1a;
    --bg-secondary: #2d2d2d;
    --text-primary: #f0f0f0;
    --text-secondary: #b0b0b0;
    --border-color: #404040;
    --accent-color: #4fc3f7;
    --shadow: 0 2px 8px rgba(0,0,0,0.5);
}

/* Blue theme */
[data-theme="blue"] {
    --bg-primary: #e3f2fd;
    --bg-secondary: #bbdefb;
    --text-primary: #0d47a1;
    --text-secondary: #1565c0;
    --border-color: #90caf9;
    --accent-color: #2196f3;
    --shadow: 0 2px 8px rgba(33,150,243,0.2);
}

/* Purple theme */
[data-theme="purple"] {
    --bg-primary: #f3e5f5;
    --bg-secondary: #e1bee7;
    --text-primary: #4a148c;
    --text-secondary: #6a1b9a;
    --border-color: #ce93d8;
    --accent-color: #9c27b0;
    --shadow: 0 2px 8px rgba(156,39,176,0.2);
}

/* Apply theme variables */
body {
    background: var(--bg-primary);
    color: var(--text-primary);
    transition: background 0.3s, color 0.3s;
}

.card {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow);
    color: var(--text-primary);
}

.link {
    color: var(--accent-color);
}

.button-primary {
    background: var(--accent-color);
    color: var(--bg-primary);
}

/* JavaScript to switch themes */
/*
function setTheme(themeName) {
    document.documentElement.setAttribute('data-theme', themeName);
    localStorage.setItem('theme', themeName);
}

// Load saved theme
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);

// Theme switcher
document.getElementById('theme-select').addEventListener('change', (e) => {
    setTheme(e.target.value);
});
*/

Example: Dynamic color palette generation

/* Base color with variants */
:root {
    --brand-hue: 210;
    --brand-saturation: 100%;
    --brand-lightness: 50%;
    
    /* Generate color palette */
    --brand-50: hsl(var(--brand-hue), var(--brand-saturation), 95%);
    --brand-100: hsl(var(--brand-hue), var(--brand-saturation), 90%);
    --brand-200: hsl(var(--brand-hue), var(--brand-saturation), 80%);
    --brand-300: hsl(var(--brand-hue), var(--brand-saturation), 70%);
    --brand-400: hsl(var(--brand-hue), var(--brand-saturation), 60%);
    --brand-500: hsl(var(--brand-hue), var(--brand-saturation), 50%);
    --brand-600: hsl(var(--brand-hue), var(--brand-saturation), 40%);
    --brand-700: hsl(var(--brand-hue), var(--brand-saturation), 30%);
    --brand-800: hsl(var(--brand-hue), var(--brand-saturation), 20%);
    --brand-900: hsl(var(--brand-hue), var(--brand-saturation), 10%);
}

/* Change theme by updating hue */
[data-theme="blue"] {
    --brand-hue: 210;
}

[data-theme="green"] {
    --brand-hue: 150;
}

[data-theme="purple"] {
    --brand-hue: 280;
}

[data-theme="orange"] {
    --brand-hue: 30;
}

/* Use generated colors */
.button {
    background: var(--brand-500);
    color: white;
}

.button:hover {
    background: var(--brand-600);
}

.alert {
    background: var(--brand-50);
    border: 1px solid var(--brand-200);
    color: var(--brand-900);
}

/* JavaScript to set custom hue */
/*
function setCustomTheme(hue) {
    document.documentElement.style.setProperty('--brand-hue', hue);
}

// Color picker
document.getElementById('hue-slider').addEventListener('input', (e) => {
    setCustomTheme(e.target.value);
});
*/

Example: Context-based theming

/* Base component variables */
.section {
    --section-bg: var(--bg-primary);
    --section-text: var(--text-primary);
    --section-accent: var(--accent-color);
    
    background: var(--section-bg);
    color: var(--section-text);
    padding: 3rem 2rem;
}

/* Dark variant */
.section-dark {
    --section-bg: #1a1a1a;
    --section-text: #f0f0f0;
    --section-accent: #4fc3f7;
}

/* Colored variants */
.section-blue {
    --section-bg: #e3f2fd;
    --section-text: #0d47a1;
    --section-accent: #2196f3;
}

.section-green {
    --section-bg: #e8f5e9;
    --section-text: #1b5e20;
    --section-accent: #4caf50;
}

/* Nested components inherit context */
.section .card {
    background: color-mix(in srgb, var(--section-bg) 95%, var(--section-text) 5%);
    border: 1px solid color-mix(in srgb, var(--section-bg) 80%, var(--section-text) 20%);
}

.section .button {
    background: var(--section-accent);
    color: var(--section-bg);
}

/* Responsive theming */
@media (max-width: 768px) {
    :root {
        --spacing-unit: 6px;
        --font-size-base: 14px;
    }
}

@media (min-width: 1024px) {
    :root {
        --spacing-unit: 10px;
        --font-size-base: 16px;
    }
}

/* Preference-based theming */
@media (prefers-contrast: high) {
    :root {
        --border-width: 2px;
        --text-primary: #000;
        --bg-primary: #fff;
    }
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg-primary: #1a1a1a;
        --text-primary: #f0f0f0;
    }
}
Note: Use data-theme or class-based theming for user preferences. Store theme choice in localStorage. Generate color scales from HSL variables. Use color-mix() for derived colors. Support system preferences with prefers-color-scheme.

3. CSS Variable Scope and Inheritance

Scope Declaration Visibility Use Case
:root :root { --var: value; } Global (entire document) Site-wide design tokens
Element selector .class { --var: value; } Element and descendants Component-specific values
Inline style style="--var: value" Element and descendants One-off overrides
Media query @media { :root { --var: val; }} Conditional global scope Responsive design tokens
Container query @container { --var: val; } Container-based scope Container-responsive values
Inheritance Child inherits from parent Down the DOM tree Cascading theme values
Override Redeclare at any level Higher specificity wins Context-specific values

Example: Variable scope and inheritance

/* Global scope */
:root {
    --primary: #007acc;
    --spacing: 1rem;
    --font-size: 16px;
}

/* Component scope */
.card {
    --card-padding: calc(var(--spacing) * 2);
    --card-bg: white;
    
    padding: var(--card-padding);
    background: var(--card-bg);
}

/* Nested override */
.card-featured {
    --card-padding: calc(var(--spacing) * 3);
    --card-bg: #f0f7ff;
}

/* Children inherit parent variables */
.card .title {
    /* Has access to --card-padding and --card-bg */
    margin-bottom: calc(var(--card-padding) / 2);
}

/* Inline style has highest specificity */
/* <div class="card" style="--card-bg: red"> */

/* Scope examples */
.sidebar {
    --sidebar-width: 250px;
}

.sidebar .nav-item {
    /* Can use --sidebar-width */
    padding-left: calc(var(--sidebar-width) / 10);
}

/* Variable not available outside scope */
.main-content {
    /* --sidebar-width is undefined here */
    width: calc(100% - var(--sidebar-width, 0px)); /* Fallback needed */
}

/* Media query scope */
@media (max-width: 768px) {
    :root {
        --spacing: 0.75rem;  /* Override global */
        --sidebar-width: 200px;  /* New responsive variable */
    }
}

/* Container query scope */
.container {
    container-type: inline-size;
}

@container (min-width: 500px) {
    .container {
        --columns: 2;
    }
}

@container (min-width: 800px) {
    .container {
        --columns: 3;
    }
}

.grid {
    grid-template-columns: repeat(var(--columns, 1), 1fr);
}

Example: Inheritance patterns and overrides

/* Theme base */
:root {
    --bg: white;
    --text: black;
    --accent: blue;
}

/* Section overrides */
.hero-section {
    --bg: #1a1a1a;
    --text: white;
    --accent: #4fc3f7;
}

/* All children inherit */
.hero-section .heading {
    color: var(--text);  /* white */
}

.hero-section .button {
    background: var(--accent);  /* #4fc3f7 */
}

/* Further override in specific component */
.hero-section .button-secondary {
    --accent: var(--text);  /* Use white as accent */
    background: transparent;
    border: 2px solid var(--accent);
    color: var(--accent);
}

/* Cascade and specificity */
.component {
    --size: 1rem;
}

.component-large {
    --size: 1.5rem;  /* More specific, wins */
}

/* Inline style beats all */
/* <div class="component component-large" style="--size: 2rem"> */
/* --size will be 2rem */

/* Practical example: Nested theme contexts */
.app {
    --theme-bg: white;
    --theme-text: black;
    background: var(--theme-bg);
    color: var(--theme-text);
}

.sidebar {
    --theme-bg: #f5f5f5;
    --theme-text: #333;
    background: var(--theme-bg);
}

.sidebar .nav-item {
    /* Inherits sidebar's theme */
    background: color-mix(in srgb, var(--theme-bg) 90%, var(--theme-text) 10%);
}

.modal {
    /* Modal creates new theme context */
    --theme-bg: white;
    --theme-text: #1a1a1a;
    background: var(--theme-bg);
    color: var(--theme-text);
}

/* Variable doesn't leak out of scope */
.isolated {
    --local-var: red;
}

.sibling {
    /* --local-var is not available here */
    color: var(--local-var, blue);  /* Uses fallback: blue */
}

Example: JavaScript manipulation of variables

/* CSS setup */
:root {
    --primary-hue: 210;
    --primary: hsl(var(--primary-hue), 100%, 50%);
}

.element {
    background: var(--primary);
}

/* JavaScript variable manipulation */
/*
// Get variable value
const root = document.documentElement;
const primaryHue = getComputedStyle(root).getPropertyValue('--primary-hue');

// Set variable value (global)
root.style.setProperty('--primary-hue', '150');

// Set variable on specific element
const element = document.querySelector('.element');
element.style.setProperty('--primary', '#ff0000');

// Remove variable
root.style.removeProperty('--primary-hue');

// Check if variable exists
const styles = getComputedStyle(root);
const hasVar = styles.getPropertyValue('--primary-hue') !== '';

// Animate variable with JavaScript
let hue = 0;
function animateHue() {
    hue = (hue + 1) % 360;
    root.style.setProperty('--primary-hue', hue);
    requestAnimationFrame(animateHue);
}

// Update multiple variables
function setTheme(colors) {
    Object.entries(colors).forEach(([key, value]) => {
        root.style.setProperty(`--${key}`, value);
    });
}

setTheme({
    'primary': '#007acc',
    'secondary': '#5c2d91',
    'background': '#ffffff'
});

// Read all custom properties
const allProps = Array.from(document.styleSheets)
    .flatMap(sheet => Array.from(sheet.cssRules))
    .filter(rule => rule.selectorText === ':root')
    .flatMap(rule => Array.from(rule.style))
    .filter(prop => prop.startsWith('--'));
*/
Warning: Variables don't work in media query conditions. Use @supports for feature detection. Invalid values make the property invalid (not ignored). Variables are case-sensitive. Circular references cause invalid values.

4. CSS Color Schemes (light/dark mode)

Property/Function Syntax Description Browser Support
color-scheme light | dark | light dark Declare supported color schemes All modern browsers
prefers-color-scheme @media (prefers-color-scheme: dark) Detect user's system preference All modern browsers
light-dark() NEW light-dark(light-val, dark-val) Automatic light/dark value selection Chrome 123+, Safari 17.5+
Meta tag <meta name="color-scheme"> Set document color scheme in HTML Affects browser UI
System colors Canvas, CanvasText, LinkText CSS system color keywords Adapt to color scheme automatically

Example: Complete light/dark mode implementation

<!-- HTML: Enable color scheme support -->
<!-- <meta name="color-scheme" content="light dark"> -->

/* CSS: Declare support for both schemes */
:root {
    color-scheme: light dark;
}

/* Method 1: Media queries (traditional) */
:root {
    --bg: white;
    --text: #1a1a1a;
    --border: #e0e0e0;
    --accent: #007acc;
    --shadow: rgba(0,0,0,0.1);
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #1a1a1a;
        --text: #f0f0f0;
        --border: #404040;
        --accent: #4fc3f7;
        --shadow: rgba(0,0,0,0.5);
    }
}

body {
    background: var(--bg);
    color: var(--text);
    border-color: var(--border);
}

/* Method 2: light-dark() function (modern) */
:root {
    color-scheme: light dark;
    
    --bg: light-dark(white, #1a1a1a);
    --text: light-dark(#1a1a1a, #f0f0f0);
    --border: light-dark(#e0e0e0, #404040);
    --accent: light-dark(#007acc, #4fc3f7);
}

/* Direct usage */
.element {
    background: light-dark(white, #1a1a1a);
    color: light-dark(#333, #f0f0f0);
    border: 1px solid light-dark(#ddd, #444);
}

/* Combine with variables */
.card {
    background: var(--bg);
    color: var(--text);
    box-shadow: 0 2px 8px var(--shadow);
}

/* System colors (automatic adaptation) */
.native-style {
    background: Canvas;
    color: CanvasText;
    border: 1px solid FieldBorder;
}

.link {
    color: LinkText;
}

/* Manual dark mode toggle */
[data-theme="light"] {
    color-scheme: light;
    --bg: white;
    --text: #1a1a1a;
}

[data-theme="dark"] {
    color-scheme: dark;
    --bg: #1a1a1a;
    --text: #f0f0f0;
}

/* Auto follows system */
[data-theme="auto"] {
    color-scheme: light dark;
}

Example: Advanced dark mode patterns

/* Semantic color system */
:root {
    color-scheme: light dark;
    
    /* Backgrounds */
    --bg-primary: light-dark(#ffffff, #0a0a0a);
    --bg-secondary: light-dark(#f5f5f5, #1a1a1a);
    --bg-tertiary: light-dark(#e0e0e0, #2d2d2d);
    
    /* Text */
    --text-primary: light-dark(#1a1a1a, #f0f0f0);
    --text-secondary: light-dark(#666666, #b0b0b0);
    --text-tertiary: light-dark(#999999, #808080);
    
    /* Borders */
    --border-primary: light-dark(#e0e0e0, #404040);
    --border-secondary: light-dark(#f0f0f0, #2d2d2d);
    
    /* Accent colors */
    --accent-primary: light-dark(#007acc, #4fc3f7);
    --accent-secondary: light-dark(#5c2d91, #ba68c8);
    --success: light-dark(#28a745, #4caf50);
    --warning: light-dark(#ffc107, #ffb300);
    --error: light-dark(#dc3545, #f44336);
    
    /* Shadows */
    --shadow-sm: light-dark(0 1px 2px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.3));
    --shadow-md: light-dark(0 4px 6px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.4));
    --shadow-lg: light-dark(0 10px 15px rgba(0,0,0,0.1), 0 10px 15px rgba(0,0,0,0.5));
}

/* Component styles */
.card {
    background: var(--bg-secondary);
    color: var(--text-primary);
    border: 1px solid var(--border-primary);
    box-shadow: var(--shadow-md);
}

.button-primary {
    background: var(--accent-primary);
    color: var(--bg-primary);
}

/* Images and media */
img {
    /* Reduce brightness in dark mode */
    filter: light-dark(none, brightness(0.8) contrast(1.2));
}

/* Code blocks */
pre {
    background: light-dark(#f6f8fa, #161b22);
    color: light-dark(#24292e, #c9d1d9);
    border: 1px solid light-dark(#d0d7de, #30363d);
}

/* Syntax highlighting */
.token.comment {
    color: light-dark(#6a737d, #8b949e);
}

.token.keyword {
    color: light-dark(#d73a49, #ff7b72);
}

.token.string {
    color: light-dark(#032f62, #a5d6ff);
}

/* Preserve colors (don't adapt) */
.logo {
    /* Brand colors stay the same */
    color: #007acc;
}

/* JavaScript theme toggle */
/*
function setColorScheme(scheme) {
    document.documentElement.style.colorScheme = scheme;
    localStorage.setItem('color-scheme', scheme);
}

// Initialize from saved preference
const saved = localStorage.getItem('color-scheme') || 'light dark';
setColorScheme(saved);

// Toggle function
function toggleDarkMode() {
    const current = document.documentElement.style.colorScheme;
    const newScheme = current === 'dark' ? 'light' : 'dark';
    setColorScheme(newScheme);
}
*/

Example: Hybrid approach (system + manual)

/* Support both system preference and manual toggle */
:root {
    /* Default to system preference */
    color-scheme: light dark;
}

/* Light mode colors */
:root,
[data-theme="light"] {
    --bg: white;
    --text: #1a1a1a;
    --border: #e0e0e0;
}

/* Dark mode from system preference */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #1a1a1a;
        --text: #f0f0f0;
        --border: #404040;
    }
}

/* Manual dark mode override */
[data-theme="dark"] {
    color-scheme: dark;
    --bg: #1a1a1a;
    --text: #f0f0f0;
    --border: #404040;
}

/* Apply colors */
body {
    background: var(--bg);
    color: var(--text);
    transition: background 0.3s, color 0.3s;
}

/* JavaScript for three-state toggle */
/*
// States: 'auto' (system), 'light', 'dark'
function setTheme(theme) {
    if (theme === 'auto') {
        document.documentElement.removeAttribute('data-theme');
        document.documentElement.style.colorScheme = 'light dark';
    } else {
        document.documentElement.setAttribute('data-theme', theme);
        document.documentElement.style.colorScheme = theme;
    }
    localStorage.setItem('theme', theme);
}

// Initialize
const savedTheme = localStorage.getItem('theme') || 'auto';
setTheme(savedTheme);

// Three-way toggle: auto → light → dark → auto
function cycleTheme() {
    const current = localStorage.getItem('theme') || 'auto';
    const next = current === 'auto' ? 'light' : 
                 current === 'light' ? 'dark' : 'auto';
    setTheme(next);
}

// Detect system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
    if (localStorage.getItem('theme') === 'auto') {
        // Refresh when system preference changes
        console.log('System theme changed to:', e.matches ? 'dark' : 'light');
    }
});
*/
Note: Set color-scheme: light dark in CSS and <meta name="color-scheme" content="light dark"> in HTML. Use light-dark() for modern browsers or prefers-color-scheme media query for wider support. Store user preference in localStorage. Support three modes: auto (system), light, dark.

5. CSS Environment Variables

Variable Description Common Values Use Case
safe-area-inset-top Top safe area inset (notches, status bar) 0px to ~44px on iOS Avoid notch areas
safe-area-inset-right Right safe area inset 0px typically Landscape mode insets
safe-area-inset-bottom Bottom safe area inset (home indicator) 0px to ~34px on iOS Avoid home indicator
safe-area-inset-left Left safe area inset 0px typically Landscape mode insets
titlebar-area-x PWA titlebar area X position Varies by OS Desktop PWA window controls
titlebar-area-y PWA titlebar area Y position Varies by OS Desktop PWA window controls
titlebar-area-width PWA titlebar area width Varies by OS Desktop PWA window controls
titlebar-area-height PWA titlebar area height Varies by OS Desktop PWA window controls

Example: Safe area insets for mobile devices

/* Handle iPhone notch and home indicator */
body {
    /* Add padding to avoid safe areas */
    padding-top: env(safe-area-inset-top);
    padding-right: env(safe-area-inset-right);
    padding-bottom: env(safe-area-inset-bottom);
    padding-left: env(safe-area-inset-left);
}

/* Fixed header that respects safe areas */
.header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    /* Add safe area to existing padding */
    padding-top: calc(1rem + env(safe-area-inset-top));
    padding-left: calc(1rem + env(safe-area-inset-left));
    padding-right: calc(1rem + env(safe-area-inset-right));
    background: white;
    z-index: 100;
}

/* Fixed footer */
.footer {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding-bottom: calc(1rem + env(safe-area-inset-bottom));
    padding-left: calc(1rem + env(safe-area-inset-left));
    padding-right: calc(1rem + env(safe-area-inset-right));
    background: white;
}

/* Full-screen modal */
.modal-fullscreen {
    position: fixed;
    inset: 0;
    /* Inset content from safe areas */
    padding: env(safe-area-inset-top) 
             env(safe-area-inset-right) 
             env(safe-area-inset-bottom) 
             env(safe-area-inset-left);
}

/* With fallback for non-supporting browsers */
.element {
    padding-top: env(safe-area-inset-top, 0px);
    /* Fallback to 0px if env() not supported */
}

/* Combine with max() for minimum padding */
.container {
    padding-top: max(1rem, env(safe-area-inset-top));
    padding-bottom: max(1rem, env(safe-area-inset-bottom));
    /* Ensures at least 1rem padding */
}

/* Sticky element at bottom */
.sticky-cta {
    position: sticky;
    bottom: env(safe-area-inset-bottom);
    padding: 1rem;
    background: #007acc;
    color: white;
}

/* Full viewport height accounting for safe areas */
.full-height {
    min-height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
}

/* Landscape mode handling */
@media (orientation: landscape) {
    .sidebar {
        padding-left: calc(1rem + env(safe-area-inset-left));
        padding-right: calc(1rem + env(safe-area-inset-right));
    }
}

Example: PWA titlebar area variables

/* Desktop PWA with custom titlebar */
/* Requires: "display_override": ["window-controls-overlay"] in manifest */

.app-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 48px;
    /* Avoid window controls area */
    padding-left: calc(1rem + env(titlebar-area-x, 0px));
    padding-right: 1rem;
    display: flex;
    align-items: center;
    background: var(--header-bg);
    -webkit-app-region: drag;  /* Make draggable */
}

/* Logo and buttons shouldn't be draggable */
.app-header .logo,
.app-header button {
    -webkit-app-region: no-drag;
}

/* Account for titlebar height */
.app-content {
    margin-top: calc(48px + env(titlebar-area-height, 0px));
}

/* Responsive titlebar */
.custom-titlebar {
    /* Use available space */
    width: calc(100vw - env(titlebar-area-width, 0px));
    height: env(titlebar-area-height, 48px);
    display: flex;
    align-items: center;
}

/* Fallback for non-PWA */
@supports not (padding: env(titlebar-area-x)) {
    .app-header {
        padding-left: 1rem;
    }
}

Example: Custom environment variable fallback patterns

/* Safe patterns with fallbacks */
.element {
    /* Pattern 1: Fallback to 0 */
    padding-top: env(safe-area-inset-top, 0px);
    
    /* Pattern 2: Fallback to custom property */
    padding-top: env(safe-area-inset-top, var(--default-padding));
    
    /* Pattern 3: Use max() for minimum */
    padding-top: max(20px, env(safe-area-inset-top));
    
    /* Pattern 4: Add to existing value */
    padding-top: calc(20px + env(safe-area-inset-top, 0px));
}

/* Complete safe area support */
:root {
    /* Create custom properties from env() */
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-right: env(safe-area-inset-right, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
    --safe-left: env(safe-area-inset-left, 0px);
}

.app-container {
    padding-top: var(--safe-top);
    padding-right: var(--safe-right);
    padding-bottom: var(--safe-bottom);
    padding-left: var(--safe-left);
}

/* Conditional env() usage */
@supports (padding: env(safe-area-inset-top)) {
    .ios-safe {
        padding-top: env(safe-area-inset-top);
    }
}

/* viewport-fit meta tag required for iOS */
/* <meta name="viewport" content="viewport-fit=cover"> */

/* Test if env() is available */
/*
const supportsEnv = CSS.supports('padding', 'env(safe-area-inset-top)');
if (supportsEnv) {
    console.log('Environment variables supported');
}

// Read env() value (returns computed value)
const style = getComputedStyle(document.documentElement);
const topInset = style.getPropertyValue('--safe-top');
console.log('Safe area top:', topInset);
*/

CSS Custom Properties & Theming Best Practices

  • Use :root for global design tokens, component selectors for scoped variables
  • Name variables semantically: --color-primary not --blue
  • Provide fallback values: var(--color, blue)
  • Generate color scales from HSL variables for dynamic theming
  • Use data-theme or class-based switching for user themes
  • Support system preferences with prefers-color-scheme
  • Use light-dark() function for modern automatic theme adaptation
  • Set color-scheme: light dark for proper browser UI
  • Always use env(safe-area-inset-*) for mobile PWAs
  • Store theme preference in localStorage for persistence