DNAðŸŽĻ CSSModern CSS Features
👑ApexCSSModern CSSArchitecture

Modern CSS Features

CSS has evolved more in the last 3 years than the previous 10. Native nesting, :has(), @scope, anchor positioning, and typed custom properties are rewriting what's possible without JavaScript.

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);
  }
}
FeatureChromeFirefoxSafariFallback Strategy
Nesting120+117+17.2+Preprocessor or flat selectors
:has()105+121+15.4+JavaScript class toggling
@scope118+Nightly17.4+BEM naming conventions
Subgrid117+71+16.0+Manual track sizing
@property85+128+15.4+Untyped custom properties
color-mix()111+113+16.2+Pre-computed color values
Anchor positioning125+❌❌Floating UI / Popper.js
@starting-style117+❌17.5+JavaScript entry animations
Scroll-driven animations115+❌❌IntersectionObserver + WAAPI

Interview Signal

Senior candidates demonstrate:

  1. Feature selection — Knowing which modern features are production-ready vs experimental, and choosing accordingly
  2. Progressive enhancement — Using @supports to layer modern features on top of solid baselines
  3. JavaScript displacement — Identifying patterns where modern CSS replaces JavaScript (:has() for state, anchor positioning for tooltips, @starting-style for entry animations)
  4. @property depth — Understanding why typed custom properties enable transitions that untyped ones can't
  5. Architecture impact — Seeing how @scope, nesting, and @layer together create a scoping model that rivals CSS-in-JS without runtime cost