CSS Custom Properties: Advanced Responsive Techniques
Beyond Media Queries
CSS Custom Properties enable more sophisticated responsive design techniques beyond traditional media queries. This guide explores advanced approaches to create adaptable layouts and components.
Responsive Values vs. Responsive Rules
Traditional responsive design changes entire CSS rules at breakpoints, while custom properties allow you to change just the values:
/* Traditional approach with media queries */
.card {
padding: 15px;
border-radius: 4px;
font-size: 14px;
}
@media (min-width: 768px) {
.card {
padding: 20px;
border-radius: 6px;
font-size: 16px;
}
}
@media (min-width: 1200px) {
.card {
padding: 30px;
border-radius: 8px;
font-size: 18px;
}
}
/* Custom properties approach */
:root {
--card-padding: 15px;
--card-border-radius: 4px;
--card-font-size: 14px;
}
@media (min-width: 768px) {
:root {
--card-padding: 20px;
--card-border-radius: 6px;
--card-font-size: 16px;
}
}
@media (min-width: 1200px) {
:root {
--card-padding: 30px;
--card-border-radius: 8px;
--card-font-size: 18px;
}
}
.card {
padding: var(--card-padding);
border-radius: var(--card-border-radius);
font-size: var(--card-font-size);
}
Fluid Typography and Spacing
Fluid Typography with calc()
Create smoothly scaling typography without breakpoints:
:root {
/* Base sizes at minimum viewport width */
--font-size-base-min: 16px;
--font-size-heading-min: 24px;
/* Base sizes at maximum viewport width */
--font-size-base-max: 20px;
--font-size-heading-max: 36px;
/* Viewport size range */
--viewport-min: 320px;
--viewport-max: 1200px;
/* Fluid typography formula */
--font-size-base: calc(
var(--font-size-base-min) +
(var(--font-size-base-max) - var(--font-size-base-min)) *
(100vw - var(--viewport-min)) /
(var(--viewport-max) - var(--viewport-min))
);
--font-size-heading: calc(
var(--font-size-heading-min) +
(var(--font-size-heading-max) - var(--font-size-heading-min)) *
(100vw - var(--viewport-min)) /
(var(--viewport-max) - var(--viewport-min))
);
}
/* Apply fluid typography with min/max clamping */
body {
font-size: clamp(
var(--font-size-base-min),
var(--font-size-base),
var(--font-size-base-max)
);
}
h1, h2, h3 {
font-size: clamp(
var(--font-size-heading-min),
var(--font-size-heading),
var(--font-size-heading-max)
);
}
Fluid Spacing System
Create a spacing system that scales with viewport size:
:root {
/* Base spacing unit at minimum viewport */
--spacing-unit-min: 8px;
/* Base spacing unit at maximum viewport */
--spacing-unit-max: 16px;
/* Viewport size range */
--viewport-min: 320px;
--viewport-max: 1200px;
/* Fluid spacing calculation */
--spacing-unit: calc(
var(--spacing-unit-min) +
(var(--spacing-unit-max) - var(--spacing-unit-min)) *
(100vw - var(--viewport-min)) /
(var(--viewport-max) - var(--viewport-min))
);
/* Spacing scale */
--spacing-xs: calc(var(--spacing-unit) * 0.5);
--spacing-sm: var(--spacing-unit);
--spacing-md: calc(var(--spacing-unit) * 2);
--spacing-lg: calc(var(--spacing-unit) * 3);
--spacing-xl: calc(var(--spacing-unit) * 4);
}
/* Apply fluid spacing */
.card {
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.button {
padding: var(--spacing-xs) var(--spacing-sm);
margin-right: var(--spacing-xs);
}
Using clamp() for Fluid Values
The clamp()
function provides a more concise way to create fluid values with minimum and maximum constraints:
:root {
/* Fluid font sizes with clamp */
--font-size-sm: clamp(14px, calc(14px + 0.25vw), 16px);
--font-size-base: clamp(16px, calc(16px + 0.5vw), 20px);
--font-size-lg: clamp(20px, calc(20px + 1vw), 28px);
--font-size-xl: clamp(24px, calc(24px + 2vw), 42px);
/* Fluid spacing with clamp */
--spacing-unit: clamp(8px, calc(8px + 0.5vw), 16px);
/* Fluid container width */
--container-width: clamp(320px, 90vw, 1200px);
}
Responsive Layouts with Custom Properties
Grid System with Custom Properties
Create a flexible grid system that adapts to viewport size:
:root {
/* Grid configuration */
--grid-columns: 4;
--grid-gap: var(--spacing-unit);
/* Responsive adjustments */
@media (min-width: 768px) {
--grid-columns: 8;
}
@media (min-width: 1200px) {
--grid-columns: 12;
}
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--grid-gap);
}
/* Column spans */
.col-1 { grid-column: span 1; }
.col-2 { grid-column: span 2; }
.col-3 { grid-column: span 3; }
.col-4 { grid-column: span 4; }
/* Responsive column spans */
@media (min-width: 768px) {
.col-md-2 { grid-column: span 2; }
.col-md-4 { grid-column: span 4; }
.col-md-6 { grid-column: span 6; }
.col-md-8 { grid-column: span 8; }
}
@media (min-width: 1200px) {
.col-lg-3 { grid-column: span 3; }
.col-lg-4 { grid-column: span 4; }
.col-lg-6 { grid-column: span 6; }
.col-lg-9 { grid-column: span 9; }
}
Responsive Component Layouts
Adjust component layouts based on viewport size:
:root {
/* Card layout properties */
--card-layout: block;
--card-image-width: 100%;
--card-body-width: 100%;
/* Responsive adjustments */
@media (min-width: 768px) {
--card-layout: grid;
--card-image-width: 40%;
--card-body-width: 60%;
}
}
.card {
display: var(--card-layout);
grid-template-columns: var(--card-image-width) var(--card-body-width);
gap: var(--spacing-md);
}
.card-image {
width: var(--card-image-width);
}
.card-body {
width: var(--card-body-width);
}
Container Queries with Custom Properties
Emulating Container Queries
While native container queries are becoming available, you can emulate some of their functionality with custom properties and JavaScript:
/* CSS */
.container {
--container-width: 0;
}
.component {
/* Apply different styles based on container width */
--component-direction: column;
--component-text-align: center;
/* When container is wider, change layout */
--component-direction-wide: row;
--component-text-align-wide: left;
/* Use the appropriate value based on container width */
flex-direction: var(--component-direction);
text-align: var(--component-text-align);
}
/* JavaScript to measure containers and set custom properties */
const containers = document.querySelectorAll('.container');
// Initial measurement
measureContainers();
// Update on resize
window.addEventListener('resize', measureContainers);
function measureContainers() {
containers.forEach(container => {
const width = container.offsetWidth;
// Set the container width as a custom property
container.style.setProperty('--container-width', `${width}px`);
// Find all components within this container
const components = container.querySelectorAll('.component');
components.forEach(component => {
// Apply container-based styles
if (width >= 500) {
component.style.setProperty('--component-direction',
getComputedStyle(component).getPropertyValue('--component-direction-wide'));
component.style.setProperty('--component-text-align',
getComputedStyle(component).getPropertyValue('--component-text-align-wide'));
} else {
component.style.removeProperty('--component-direction');
component.style.removeProperty('--component-text-align');
}
});
});
}
Native Container Queries with Custom Properties
As browser support improves, you can combine native container queries with custom properties:
/* Define the container */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Base card properties */
.card {
--card-layout: block;
--card-image-width: 100%;
--card-content-width: 100%;
--card-padding: var(--spacing-md);
--card-gap: var(--spacing-sm);
}
/* Container query to adjust properties */
@container card (min-width: 400px) {
.card {
--card-layout: grid;
--card-image-width: 40%;
--card-content-width: 60%;
--card-padding: var(--spacing-lg);
--card-gap: var(--spacing-md);
}
}
@container card (min-width: 600px) {
.card {
--card-image-width: 30%;
--card-content-width: 70%;
}
}
/* Apply the properties */
.card {
display: var(--card-layout);
grid-template-columns: var(--card-image-width) var(--card-content-width);
gap: var(--card-gap);
padding: var(--card-padding);
}
Viewport-Aware Components
Viewport Height Adjustments
Create components that respond to viewport height:
:root {
/* Detect viewport height ranges */
--is-short-viewport: 0;
--is-medium-viewport: 0;
--is-tall-viewport: 0;
/* Default for short viewports */
--header-height: 60px;
--footer-height: 60px;
--main-padding: var(--spacing-md);
/* Adjust for medium height viewports */
@media (min-height: 700px) {
--is-short-viewport: 0;
--is-medium-viewport: 1;
--is-tall-viewport: 0;
--header-height: 80px;
--footer-height: 80px;
--main-padding: var(--spacing-lg);
}
/* Adjust for tall viewports */
@media (min-height: 1000px) {
--is-short-viewport: 0;
--is-medium-viewport: 0;
--is-tall-viewport: 1;
--header-height: 100px;
--footer-height: 100px;
--main-padding: var(--spacing-xl);
}
}
.header {
height: var(--header-height);
}
.footer {
height: var(--footer-height);
}
.main {
min-height: calc(100vh - var(--header-height) - var(--footer-height));
padding: var(--main-padding);
}
/* Conditional styles based on viewport height */
.hero-section {
height: calc(var(--is-short-viewport) * 50vh +
var(--is-medium-viewport) * 70vh +
var(--is-tall-viewport) * 80vh);
}
Orientation-Aware Layouts
Adjust layouts based on device orientation:
:root {
/* Default (portrait) layout */
--gallery-columns: 2;
--sidebar-width: 100%;
--main-width: 100%;
--layout-direction: column;
/* Landscape adjustments */
@media (orientation: landscape) {
--gallery-columns: 4;
--sidebar-width: 30%;
--main-width: 70%;
--layout-direction: row;
}
}
.gallery {
display: grid;
grid-template-columns: repeat(var(--gallery-columns), 1fr);
gap: var(--spacing-sm);
}
.layout {
display: flex;
flex-direction: var(--layout-direction);
}
.sidebar {
width: var(--sidebar-width);
}
.main-content {
width: var(--main-width);
}
Device Feature Detection
Touch-Optimized Interfaces
Adjust UI elements for touch devices:
:root {
/* Default values for non-touch devices */
--button-size: 36px;
--input-height: 36px;
--checkbox-size: 16px;
--slider-height: 8px;
--slider-thumb-size: 16px;
/* Values for touch devices */
@media (pointer: coarse) {
--button-size: 44px;
--input-height: 44px;
--checkbox-size: 24px;
--slider-height: 12px;
--slider-thumb-size: 24px;
}
}
.button {
min-height: var(--button-size);
min-width: var(--button-size);
}
input[type="text"],
input[type="email"],
select {
height: var(--input-height);
}
input[type="checkbox"] {
width: var(--checkbox-size);
height: var(--checkbox-size);
}
input[type="range"] {
height: var(--slider-height);
}
input[type="range"]::-webkit-slider-thumb {
width: var(--slider-thumb-size);
height: var(--slider-thumb-size);
}
High-DPI Screen Adjustments
Optimize UI for different screen densities:
:root {
/* Default values for standard screens */
--border-width: 1px;
--outline-width: 2px;
--shadow-blur: 4px;
/* High-DPI screens */
@media (-webkit-min-device-pixel-ratio: 2),
(min-resolution: 192dpi) {
--border-width: 0.5px;
--outline-width: 1px;
--shadow-blur: 2px;
}
}
.card {
border: var(--border-width) solid var(--color-border);
box-shadow: 0 2px var(--shadow-blur) rgba(0, 0, 0, 0.1);
}
.button:focus {
outline: var(--outline-width) solid var(--color-primary);
}
Reduced Motion Preferences
Respect user preferences for reduced motion:
:root {
/* Default animation values */
--transition-duration: 0.3s;
--animation-duration: 1s;
--hover-transform: scale(1.05);
/* Reduced values for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
--transition-duration: 0.1s;
--animation-duration: 0.3s;
--hover-transform: scale(1.01);
}
}
.button {
transition: all var(--transition-duration) ease;
}
.button:hover {
transform: var(--hover-transform);
}
.loading-spinner {
animation: spin var(--animation-duration) linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
Dynamic Responsive Adjustments
JavaScript Viewport Detection
Use JavaScript to set viewport-based custom properties:
// Set viewport dimensions as CSS custom properties
function updateViewportProperties() {
const vh = window.innerHeight * 0.01;
const vw = window.innerWidth * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
document.documentElement.style.setProperty('--vw', `${vw}px`);
document.documentElement.style.setProperty('--viewport-width', `${window.innerWidth}px`);
document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`);
// Set device type flags
const isMobile = window.innerWidth < 768;
const isTablet = window.innerWidth >= 768 && window.innerWidth < 1024;
const isDesktop = window.innerWidth >= 1024;
document.documentElement.style.setProperty('--is-mobile', isMobile ? 1 : 0);
document.documentElement.style.setProperty('--is-tablet', isTablet ? 1 : 0);
document.documentElement.style.setProperty('--is-desktop', isDesktop ? 1 : 0);
}
// Initial call
updateViewportProperties();
// Update on resize
window.addEventListener('resize', updateViewportProperties);
/* CSS usage */
.hero {
/* Use the accurate viewport height instead of vh units */
height: calc(var(--vh) * 100);
}
.sidebar {
/* Conditionally show/hide based on viewport */
display: calc(var(--is-mobile) * none + (1 - var(--is-mobile)) * block);
}
.mobile-menu {
/* Only show on mobile */
display: calc(var(--is-mobile) * block + (1 - var(--is-mobile)) * none);
}
Scroll-Based Responsive Adjustments
Modify layout based on scroll position:
:root {
--scroll-position: 0;
--is-scrolled: 0;
--header-height: 80px;
--header-height-scrolled: 60px;
}
/* JavaScript to track scroll position */
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const scrollPercentage = Math.min(scrollTop / 100, 1);
document.documentElement.style.setProperty('--scroll-position', scrollPercentage);
document.documentElement.style.setProperty('--is-scrolled', scrollTop > 50 ? 1 : 0);
});
/* CSS usage */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: calc(var(--header-height) -
(var(--header-height) - var(--header-height-scrolled)) *
var(--is-scrolled));
background-color: rgba(255, 255, 255, calc(0.8 + (0.2 * var(--is-scrolled))));
box-shadow: 0 2px 10px rgba(0, 0, 0, calc(0.1 * var(--is-scrolled)));
transition: height 0.3s, background-color 0.3s, box-shadow 0.3s;
}
.logo {
transform: scale(calc(1 - (0.2 * var(--is-scrolled))));
transition: transform 0.3s;
}
Best Practices
Performance Considerations
- Limit Recalculation: Changing custom properties triggers style recalculation, so be mindful of frequent updates
- Use requestAnimationFrame: When updating properties in response to scroll or resize events
- Throttle Event Handlers: Limit the frequency of updates for scroll, resize, and mousemove events
- Batch Updates: Group multiple property changes together
- Consider Hardware Acceleration: Use transform and opacity for animations when possible
Organization Tips
- Group Related Properties: Keep responsive variables organized by component or feature
- Use Descriptive Names: Name variables based on their purpose, not just their value
- Document Breakpoints: Comment your code to explain the responsive strategy
- Create a System: Develop a consistent approach to responsive custom properties
Fallback Strategies
- Provide Default Values: Always include fallbacks in var() functions
- Feature Detection: Use @supports to provide alternatives for older browsers
- Progressive Enhancement: Build a solid base experience that works without custom properties
Next Steps
Now that you understand advanced responsive techniques with CSS Custom Properties, you can explore our other guides in this series:
- CSS Custom Properties: Basics
- CSS Custom Properties: JavaScript Integration
- CSS Custom Properties: Theming Systems
- CSS Custom Properties: Integration with Preprocessors