CSS Advanced Techniques: Custom Properties, Container Queries, Grid Masonry and Modern Layouts
CSS has evolved dramatically in the past few years. The gap between what you can do with pure CSS today versus five years ago is enormous. This guide covers the modern CSS features that are changing how developers build layouts and design systems β and that increasingly come up in frontend interviews.
CSS Custom Properties (Variables)
CSS custom properties are not just variables β they are live, cascading, inherited, and can be modified with JavaScript.
css:root { --color-primary: #3b82f6; --color-secondary: #64748b; --color-background: #ffffff; --color-text: #1e293b; --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; --radius-sm: 0.25rem; --radius-md: 0.5rem; --radius-full: 9999px; --font-size-sm: 0.875rem; --font-size-base: 1rem; --font-size-lg: 1.125rem; --font-size-xl: 1.25rem; } /* Dark mode */ [data-theme="dark"] { --color-background: #0f172a; --color-text: #f1f5f9; --color-primary: #60a5fa; } .button { background: var(--color-primary); color: white; padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--radius-md); font-size: var(--font-size-base); }
Scoped custom properties
Custom properties cascade β you can scope them to components:
css.card { --card-padding: var(--spacing-md); --card-radius: var(--radius-md); --card-shadow: 0 1px 3px rgba(0,0,0,0.12); padding: var(--card-padding); border-radius: var(--card-radius); box-shadow: var(--card-shadow); } .card--compact { --card-padding: var(--spacing-sm); /* override just for compact variant */ }
CSS custom properties with JavaScript
javascript// Read a custom property const primary = getComputedStyle(document.documentElement) .getPropertyValue("--color-primary").trim(); // Set a custom property dynamically document.documentElement.style.setProperty("--color-primary", "#10b981"); // Theme switching function setTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); }
Container Queries
Media queries respond to the viewport width. Container queries respond to the container width β enabling truly reusable components that adapt to their context.
css/* Define a containment context */ .card-grid { container-type: inline-size; container-name: card-grid; } /* Style the card based on its container width, not the viewport */ .card { display: grid; grid-template-columns: 1fr; } @container card-grid (min-width: 400px) { .card { grid-template-columns: auto 1fr; gap: 1rem; } } @container card-grid (min-width: 700px) { .card { grid-template-columns: auto 1fr auto; } }
The same card component now works correctly whether it is in a narrow sidebar or a wide main column β without any JavaScript or media query hackery.
Advanced CSS Grid
Auto-fit with minmax (responsive without media queries)
css.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } /* Columns automatically adapt -- 1 column at narrow, up to N at wide */
Masonry-style layout
css/* Native CSS masonry (experimental, Chrome flags needed as of 2025) */ .masonry { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: masonry; gap: 1rem; } /* Current workaround: multi-column */ .masonry-fallback { columns: 3 250px; column-gap: 1rem; } .masonry-fallback > * { break-inside: avoid; margin-bottom: 1rem; }
Named grid areas
css.layout { display: grid; grid-template-areas: "header header header" "sidebar content aside" "footer footer footer"; grid-template-columns: 200px 1fr 200px; grid-template-rows: auto 1fr auto; min-height: 100vh; gap: 1rem; } header { grid-area: header; } .sidebar { grid-area: sidebar; } main { grid-area: content; } aside { grid-area: aside; } footer { grid-area: footer; } @media (max-width: 768px) { .layout { grid-template-areas: "header" "content" "sidebar" "aside" "footer"; grid-template-columns: 1fr; } }
The :has() Selector
:has() is the long-awaited "parent selector" β style an element based on what it contains:
css/* Style a form group that contains an invalid input */ .form-group:has(input:invalid) { background: #fef2f2; border-color: #ef4444; } /* Card with an image gets different padding */ .card:has(img) { padding: 0; } .card:has(img) .card-body { padding: var(--spacing-md); } /* Navigation with open dropdown */ .nav-item:has(.dropdown[open]) > .nav-link { color: var(--color-primary); font-weight: 600; } /* Table row that contains a checked checkbox */ tr:has(input[type="checkbox"]:checked) { background: var(--color-selected); } /* Quantity queries -- style list differently based on item count */ li:nth-child(n+4) { /* styles when there are 4+ items */ } ul:has(li:nth-child(4)) li { /* style all items when there are at least 4 */ }
:has() has excellent browser support as of 2025 β use it.
CSS Cascade Layers
@layer gives you explicit control over specificity and cascade order β solving the specificity wars of large CSS codebases:
css/* Define layer order -- lower layers have lower priority */ @layer reset, base, tokens, components, utilities; @layer reset { *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } } @layer base { body { font-family: system-ui, sans-serif; color: var(--color-text); } a { color: var(--color-primary); } } @layer components { .button { padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--radius-md); background: var(--color-primary); color: white; } } @layer utilities { .sr-only { position: absolute; width: 1px; height: 1px; clip: rect(0, 0, 0, 0); } .text-center { text-align: center; } }
Styles in utilities always beat components regardless of specificity β finally, predictable cascade control.
CSS Logical Properties
Logical properties use start/end instead of left/right, enabling correct layout for RTL languages with no extra CSS:
css/* Physical properties (language-specific) */ .card { margin-left: 1rem; margin-right: 1rem; padding-left: 1rem; border-left: 3px solid var(--color-primary); text-align: left; } /* Logical equivalents (work for both LTR and RTL) */ .card { margin-inline: 1rem; /* left and right in LTR, right and left in RTL */ padding-inline-start: 1rem; /* left in LTR, right in RTL */ border-inline-start: 3px solid var(--color-primary); text-align: start; }
Quick reference:
margin-inlineβmargin-left+margin-rightpadding-blockβpadding-top+padding-bottominset-inline-startβleft(in LTR)block-sizeβheightinline-sizeβwidth
Scroll-Driven Animations
CSS animations that respond to scroll position β no JavaScript needed:
css@keyframes fade-in-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .reveal-on-scroll { animation: fade-in-up linear both; animation-timeline: view(); /* linked to element's scroll position */ animation-range: entry 0% entry 30%; /* plays as element enters viewport */ } /* Progress bar that fills as user scrolls */ .scroll-progress { position: fixed; top: 0; left: 0; height: 3px; background: var(--color-primary); animation: grow-width linear; animation-timeline: scroll(root); } @keyframes grow-width { from { width: 0%; } to { width: 100%; } }
Common Interview Questions
Q: What is the difference between CSS custom properties and preprocessor variables (Sass)?
Sass variables are resolved at compile time β they do not exist in the output CSS. CSS custom properties are live browser values β they can be changed at runtime with JavaScript, can be scoped to elements, and participate in the cascade and inheritance. Sass variables are more powerful for build-time logic; CSS custom properties are more powerful for runtime theming.
Q: What are container queries and why are they better than media queries for component libraries?
Media queries respond to the viewport width β a component looks the same at a given viewport width regardless of where it is placed. Container queries respond to the parent container's width, enabling components to adapt based on their actual available space. This makes components truly reusable β they look right whether in a narrow sidebar or a wide main section.
Q: What does @layer solve?
Without @layer, CSS specificity battles are hard to manage in large codebases β a utility class might need !important to override a component's styles. @layer lets you define explicit cascade order: styles in later layers always win, regardless of selector specificity. This makes large CSS architectures predictable.
Practice Frontend on Froquiz
CSS layout and modern techniques are tested in frontend developer interviews. Test your CSS knowledge on Froquiz β covering Flexbox, Grid, animations, and more.
Summary
- CSS custom properties cascade and inherit β use them for design tokens and runtime theming
- Container queries let components respond to their container width, not the viewport
auto-fitwithminmaxcreates responsive grids with zero media queries:has()is the parent selector β style elements based on their contents@layergives explicit cascade priority control β eliminates specificity wars- Logical properties (
margin-inline,padding-block) support RTL layouts automatically - Scroll-driven animations link CSS animations to scroll position with no JavaScript