Modern CSS Features
The gap between CSS and what developers used preprocessors and JavaScript for is closing rapidly. Native nesting, :has(), typed custom properties, and anchor positioning eliminate entire categories of tooling dependencies. Architect-level engineers leverage these features for systems that are simpler, more performant, and more maintainable.
CSS Nesting (Native)
Native nesting brings Sass-like nesting to vanilla CSS:
.card {
padding: 1.5rem;
border-radius: 0.75rem;
background: var(--surface);
.card-title {
font-size: 1.25rem;
font-weight: 600;
}
.card-body {
margin-block-start: 0.75rem;
line-height: 1.6;
}
&:hover {
box-shadow: 0 4px 12px oklch(0 0 0 / 0.1);
}
&.card--featured {
border: 2px solid var(--color-primary);
}
@media (min-width: 48rem) {
padding: 2rem;
}
}Nesting Rules
The & is optional when nesting starts with a selector that isn't ambiguous with a property:
.parent {
color: red;
.child { color: blue; } /* â
.parent .child */
& .child { color: blue; } /* â
Same as above */
&.active { color: green; } /* â
.parent.active (no space) */
:hover { color: orange; } /* â
.parent :hover */
&:hover { color: orange; } /* â
.parent:hover (no space â different!) */
}The distinction between .parent :hover (any hovered descendant) and .parent:hover (the parent itself hovered) is critical. Use & explicitly for pseudo-classes.
:has() â The Parent Selector
:has() matches an element based on its contents. It inverts the traditional downward-only cascade:
/* Style a form group when its input has an error */
.form-group:has(:invalid) {
border-color: var(--color-error);
}
.form-group:has(:invalid) .label {
color: var(--color-error);
}
/* Style a card based on its content */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
.card:has(> .card-actions) {
padding-block-end: 0;
}
/* "Previous sibling" selector (no direct selector exists) */
.tab:has(+ .tab:hover) {
opacity: 0.7;
}
/* Conditional page layout */
body:has(.sidebar) {
grid-template-columns: 280px 1fr;
}
body:not(:has(.sidebar)) {
grid-template-columns: 1fr;
}:has() is evaluated live â it responds to DOM changes, form state, and user interaction in real time. This replaces JavaScript class toggling for many UI patterns.
@scope â Scoped Styles
@scope limits where styles apply, with both upper and lower boundaries:
@scope (.card) {
:scope {
padding: 1.5rem;
border-radius: 0.75rem;
}
.title {
font-size: 1.25rem;
}
.content {
line-height: 1.6;
}
}
/* With a lower boundary â styles don't leak into nested components */
@scope (.card) to (.card__slot) {
p {
color: var(--text-secondary);
}
}The lower boundary (to) prevents styles from penetrating into slotted or nested component regions. This solves the "styles leaking into children" problem that Shadow DOM solves more aggressively.
When two @scope rules conflict, the one with the closer scoping root wins â this is scope proximity, a new cascade resolution step.
Subgrid
Subgrid lets nested grids participate in their parent's grid tracks:
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}Without subgrid, each card defines its own row tracks â titles, bodies, and footers don't align across cards. With subgrid, all cards share the parent's row lines, creating perfect horizontal alignment across sibling grid items.
@property â Typed Custom Properties
@property registers custom properties with types, initial values, and inheritance control:
@property --hue {
syntax: '<angle>';
initial-value: 250deg;
inherits: false;
}
@property --progress {
syntax: '<number>';
initial-value: 0;
inherits: false;
}Why Typing Matters
Untyped custom properties can't be transitioned â the browser treats them as strings:
/* Without @property: snaps instantly (browser sees a string) */
:root { --hue: 250; }
.el:hover { --hue: 320; }
/* With @property: smooth transition (browser knows it's an angle) */
@property --hue {
syntax: '<angle>';
initial-value: 250deg;
inherits: false;
}
.el {
--hue: 250deg;
background: oklch(0.7 0.15 var(--hue));
transition: --hue 500ms ease;
}
.el:hover { --hue: 320deg; }This unlocks animated gradients, color shifts, and property interpolation that was previously impossible in CSS.
Color Functions
/* color-mix() â blend colors in any color space */
:root {
--primary: oklch(0.6 0.2 250);
--primary-light: color-mix(in oklch, var(--primary), white 30%);
--primary-dark: color-mix(in oklch, var(--primary), black 30%);
}
/* light-dark() â reads from color-scheme, no media query needed */
:root { color-scheme: light dark; }
.card {
background: light-dark(white, oklch(0.2 0 0));
color: light-dark(oklch(0.15 0 0), oklch(0.9 0 0));
}CSS Anchor Positioning
Anchor positioning tethers elements to other elements without JavaScript:
.trigger {
anchor-name: --tooltip-anchor;
}
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0.5rem;
/* Fallback if tooltip overflows viewport */
position-try-fallbacks: --above;
}
@position-try --above {
bottom: anchor(top);
top: auto;
translate: -50% -0.5rem;
}This replaces Popper.js / Floating UI for tooltips, dropdowns, and popovers â with automatic overflow-aware repositioning handled by the browser.
Popover API Integration
The Popover API provides built-in dismiss behavior, focus management, and top-layer rendering. Combined with anchor positioning and @starting-style, you get animated, positioned, accessible popovers with zero JavaScript:
<button popovertarget="menu">Open Menu</button>
<div id="menu" popover>
<nav>
<a href="/dashboard">Dashboard</a>
<a href="/settings">Settings</a>
</nav>
</div>[popover] {
margin: 0;
padding: 1rem;
border-radius: 0.5rem;
&:popover-open { opacity: 1; transform: scale(1); }
@starting-style {
&:popover-open { opacity: 0; transform: scale(0.95); }
}
}Other Notable Features
/* Math functions: round(), mod(), abs(), sign() */
.grid-item {
width: round(var(--dynamic-width), 8px);
transform: scaleX(sign(var(--direction)));
font-size: clamp(1rem, round(1rem + 1vw, 0.125rem), 2rem);
}
/* @media (scripting) â replaces <noscript> and .no-js patterns */
@media (scripting: none) {
.js-only { display: none; }
}
/* field-sizing â replaces JS auto-resize for textareas */
textarea {
field-sizing: content;
min-block-size: 3lh;
max-block-size: 10lh;
}Progressive Enhancement Strategy
Modern CSS features require a progressive enhancement mindset:
/* Base experience: works everywhere */
.layout {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
/* Enhancement: container queries */
@supports (container-type: inline-size) {
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { grid-template-columns: 200px 1fr; }
}
}
/* Enhancement: :has() */
@supports selector(:has(*)) {
.form-group:has(:invalid) {
border-color: var(--color-error);
}
}
/* Enhancement: anchor positioning */
@supports (anchor-name: --x) {
.tooltip {
position: fixed;
position-anchor: --trigger;
top: anchor(bottom);
}
}| Feature | Chrome | Firefox | Safari | Fallback Strategy |
|---|---|---|---|---|
| Nesting | 120+ | 117+ | 17.2+ | Preprocessor or flat selectors |
:has() | 105+ | 121+ | 15.4+ | JavaScript class toggling |
@scope | 118+ | Nightly | 17.4+ | BEM naming conventions |
| Subgrid | 117+ | 71+ | 16.0+ | Manual track sizing |
@property | 85+ | 128+ | 15.4+ | Untyped custom properties |
color-mix() | 111+ | 113+ | 16.2+ | Pre-computed color values |
| Anchor positioning | 125+ | â | â | Floating UI / Popper.js |
@starting-style | 117+ | â | 17.5+ | JavaScript entry animations |
| Scroll-driven animations | 115+ | â | â | IntersectionObserver + WAAPI |
Interview Signal
Senior candidates demonstrate:
- Feature selection â Knowing which modern features are production-ready vs experimental, and choosing accordingly
- Progressive enhancement â Using
@supportsto layer modern features on top of solid baselines - JavaScript displacement â Identifying patterns where modern CSS replaces JavaScript (
:has()for state, anchor positioning for tooltips,@starting-stylefor entry animations) @propertydepth â Understanding why typed custom properties enable transitions that untyped ones can't- Architecture impact â Seeing how
@scope, nesting, and@layertogether create a scoping model that rivals CSS-in-JS without runtime cost