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
:rootfor global design tokens, component selectors for scoped variables - Name variables semantically:
--color-primarynot--blue - Provide fallback values:
var(--color, blue) - Generate color scales from HSL variables for dynamic theming
- Use
data-themeor 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 darkfor proper browser UI - Always use
env(safe-area-inset-*)for mobile PWAs - Store theme preference in localStorage for persistence