Skip to main content

Modern CSS Features 2026: 11 Production-Ready Features

Created: March 7, 2026 Larry Qu 13 min read
Table of Contents

Introduction

CSS has entered a new era. The features shipping between 2022 and 2026 fundamentally change how we build web interfaces — replacing JavaScript workarounds with native browser primitives, eliminating preprocessor dependencies, and enabling layout patterns that were previously impossible. This guide covers 11 production-ready CSS features you can use today.

Container Queries: Component-Responsive Design

Container queries solve a fundamental limitation of media queries: they only know the viewport size, not the size of a component’s parent. A card in a narrow sidebar needs different styles from the same card in a wide main area, but media queries cannot distinguish between the two.

Defining a Container

Use container-type and container-name to turn any element into a query container:

.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

.main-content {
  container-type: inline-size;
  container-name: content;
}

Querying the Container

Once defined, @container queries the container’s inline size rather than the viewport:

.card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

@container sidebar (min-width: 400px) {
  .card {
    flex-direction: row;
    grid-template-columns: 1fr 2fr;
  }
}

When the sidebar container reaches 400px wide, the card switches from stacked to side-by-side layout. The same card in a different container with a different width responds independently.

Container Query Units

Six new units give you proportional sizing relative to the nearest container:

Unit Relative To
cqw 1% of container width
cqh 1% of container height
cqi 1% of container inline size
cqb 1% of container block size
cqmin Smaller of cqi or cqb
cqmax Larger of cqi or cqb

Use these to size typography and spacing proportionally to the container:

.card-title {
  font-size: clamp(1rem, 4cqi, 2rem);
}

.card-body {
  padding: 2cqi;
}

Real-World Pattern: Responsive Dashboard Widget

.widget {
  container-type: inline-size;
}

@container (max-width: 300px) {
  .widget {
    font-size: 0.875rem;
  }
  .widget__actions {
    display: none;
  }
}

@container (min-width: 600px) {
  .widget {
    display: grid;
    grid-template-columns: 1fr auto;
  }
}

For a deeper dive, see the full CSS Container Queries guide.

Cascade Layers: Specificity Management

Specificity wars plague every large CSS codebase. Cascade layers solve this by letting you declare priority order for groups of styles, independent of selector specificity.

Declaring Layer Order

Define the layer hierarchy once at the top of your stylesheet:

@layer reset, base, components, utilities;

Layers declared later win over earlier layers, regardless of selector specificity. A .hidden utility class with single-class specificity in the utilities layer beats a three-class selector in the components layer.

Organizing Styles Into Layers

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }
}

@layer base {
  :root {
    --color-primary: oklch(60% 0.15 250);
    --spacing-md: 1rem;
  }
  body {
    font-family: system-ui, sans-serif;
    line-height: 1.6;
  }
}

@layer components {
  .card {
    padding: var(--spacing-md);
    background: var(--color-surface);
    border-radius: 8px;
  }
}

@layer utilities {
  .hidden {
    display: none;
  }
  .p-0 {
    padding: 0;
  }
}

Nested Layers and Import

Layer names can be nested for third-party framework integration:

@layer framework {
  @layer reset, components;
}

@layer framework.reset {
  /* Framework reset styles */
}

@layer app {
  /* Your app styles — always win over framework */
}

Import external stylesheets directly into a layer:

@import url("bootstrap.css") layer(bootstrap);

:has() Selector: Parent and Conditional Styling

The :has() pseudo-class is the parent selector CSS developers requested for two decades. It styles an element based on its descendants, siblings, or state.

Parent Selection

/* Card with a featured image gets border */
.card:has(.featured-image) {
  border: 2px solid var(--color-primary);
}

/* Form group with validation error */
.form-group:has(:invalid) label {
  color: red;
}

/* List item containing a checked checkbox */
li:has(input[type="checkbox"]:checked) {
  text-decoration: line-through;
  opacity: 0.6;
}

Sibling-Aware Styling

/* Heading with a following paragraph */
h2:has(+ p) {
  margin-bottom: 0.5rem;
}

/* First card next to a featured card */
.card:has(+ .card.featured) {
  border-right: none;
}

Container Queries Without Container

One clever pattern uses :has() to simulate container-like behavior when you cannot modify the parent element:

/* Apply grid when section contains enough articles */
section:has(article:nth-child(4)) {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

/* Single-column layout when fewer items */
section:not(:has(article:nth-child(2))) {
  display: block;
}

Performance Considerations

Avoid deeply nested :has() chains. Browsers optimize simple :has() queries well, but :has(.a :has(.b :has(.c)))) creates exponential work for the selector engine.

Read the dedicated CSS :has() Selector guide for more patterns and gotchas.

CSS Nesting: No Preprocessor Required

Native CSS nesting shipped in Chrome 120+, Firefox 117+, and Safari 17.2+. It supports the familiar & parent reference and works without any build step.

Basic Nesting Syntax

.card {
  padding: 1rem;
  background: white;
  border-radius: 8px;

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  .title {
    font-size: 1.5rem;
    font-weight: 600;
  }

  p {
    color: #555;
    line-height: 1.5;
  }
}

Nesting Media Queries

Media queries nest directly inside selectors, keeping responsive variants colocated:

.card {
  padding: 1rem;

  @media (min-width: 768px) {
    padding: 2rem;
  }

  @media (prefers-color-scheme: dark) {
    background: #1a1a2e;
    color: #e0e0e0;
  }
}

The & Reference for Compound Selectors

Use & to append selectors to the parent:

.button {
  background: var(--color-primary);
  color: white;

  &.featured {
    background: var(--color-accent);
  }

  & .icon {
    margin-right: 0.5rem;
  }

  /* Compound selector: .button:focus-visible */
  &:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
  }
}

Sass vs Native CSS Nesting

Feature Sass Native CSS
Basic nesting Yes Yes
& parent reference Yes Yes
Media query nesting Yes Yes
Selector concatenation Yes Yes
Mixins Yes No
@extend Yes No
Build step required Yes No

Many teams in 2026 have dropped Sass entirely, using native CSS with PostCSS for vendor prefixes only.

Modern Color Workflows: OKLCH, color-mix(), light-dark()

The 2020s brought a color revolution to CSS. Three features form the foundation of a modern, maintainable color system.

OKLCH: Perceptually Uniform Color

OKLCH (OK Luminance Chroma Hue) is a color space designed for human perception. Equal lightness steps look equally different — unlike HSL where yellows appear brighter than blues at the same lightness value.

:root {
  /* oklch(lightness chroma hue) */
  --color-primary: oklch(60% 0.18 250);
  --color-primary-light: oklch(80% 0.18 250);
  --color-primary-dark: oklch(40% 0.18 250);
}

color-mix(): Dynamic Blending

Blend two colors in any color space without precomputing palettes:

:root {
  --brand: oklch(62% 0.16 35);
}

.button {
  background: var(--brand);
}

.button:hover {
  /* Mix with 15% white for a lighter hover */
  background: color-mix(in oklch, var(--brand) 85%, white);
}

.button:active {
  /* Mix with 20% black for press state */
  background: color-mix(in oklch, var(--brand) 80%, black);
}

Tint and shade palettes from a single base color:

:root {
  --base: oklch(60% 0.15 250);

  --tint-100: color-mix(in oklch, var(--base) 10%, white);
  --tint-200: color-mix(in oklch, var(--base) 25%, white);
  --tint-300: color-mix(in oklch, var(--base) 50%, white);

  --shade-700: color-mix(in oklch, var(--base) 75%, black);
  --shade-800: color-mix(in oklch, var(--base) 90%, black);
  --shade-900: color-mix(in oklch, var(--base) 100%, black);
}

light-dark(): Automatic Theme Adaptation

The light-dark() function eliminates the need for prefers-color-scheme media queries for most color declarations. It takes two arguments: the light mode color and the dark mode color.

:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #121212);
  color: light-dark(#1a1a2e, #e0e0e0);
}

.card {
  background: light-dark(#f5f5f7, #1e1e2e);
  border-color: light-dark(#ddd, #333);
}

The browser automatically selects the correct color based on the user’s system preference. No media query, no JavaScript, no duplicate property blocks.

Relative Color Syntax

Derive new colors from an existing one by manipulating individual channels:

:root {
  --primary: oklch(60% 0.15 250);
  --primary-light: oklch(from var(--primary) calc(l + 0.2) c h);
  --primary-muted: oklch(from var(--primary) l calc(c - 0.08) h);
  --primary-complement: oklch(from var(--primary) l c calc(h + 180));
}

@property: Typed Custom Properties

Standard CSS custom properties (--) are typeless — the browser treats them as strings, which means they cannot animate or transition. The @property at-rule gives them types, initial values, and inheritance rules.

Defining a Typed Property

@property --rotation {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --scale {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

Animating Registered Properties

Once typed, custom properties participate in CSS transitions and animations:

.box {
  --rotation: 0deg;
  transform: rotate(var(--rotation));
  transition: --rotation 0.3s ease;
}

.box:hover {
  --rotation: 45deg;
  /* Smoothly interpolates between angle values */
}

Animated Gradients

Typed properties unlock gradient animations that were previously only possible with JavaScript:

@property --gradient-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --color-stop {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 50%;
}

.gradient-box {
  --gradient-angle: 0deg;
  --color-stop: 50%;
  background: linear-gradient(var(--gradient-angle), #ff6b6b var(--color-stop), #4ecdc4);
  transition: --gradient-angle 1s, --color-stop 0.5s;
}

.gradient-box:hover {
  --gradient-angle: 180deg;
  --color-stop: 70%;
}

Syntax Values

Syntax Description Example Initial Value
<length> Any length 0px
<number> Numeric value 0
<percentage> Percentage 0%
<color> Any color value black
<angle> Angle value 0deg
<length-percentage> Length or percentage 0
<custom-ident> Custom identifier "none"
`“foo bar”` Enum of allowed values

Anchor Positioning: Tooltips and Popovers Without JavaScript

CSS anchor positioning provides a native mechanism to tether one element to another. Combined with the Popover API, it replaces the need for JavaScript-based positioning libraries.

Basic Anchor Setup

Declare the anchor element and position the target:

.trigger {
  anchor-name: --tooltip;
}

.tooltip {
  position: absolute;
  position-anchor: --tooltip;
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 4px;
}

Using the Popover API

The Popover API gives you top-layer rendering and automatic dismiss behavior:

<button popovertarget="info-popover" class="trigger">
  More info
</button>

<div id="info-popover" popover class="tooltip">
  This content renders on the top layer and dismisses on click outside.
</div>
.trigger {
  anchor-name: --info-trigger;
}

.tooltip {
  position: absolute;
  position-anchor: --info-trigger;
  position-area: top;
  margin: 0;
  padding: 0.5rem 1rem;
  background: #333;
  color: white;
  border-radius: 6px;
  border: none;
}

Fallback Positioning with @position-try

Define fallback positions that the browser tries when the primary placement overflows:

.tooltip {
  position-area: bottom;
  @position-try --try-top {
    position-area: top;
  }
  @position-try --try-right {
    position-area: right;
  }
}

The browser automatically picks the first position that keeps the element visible, eliminating overflow-checking JavaScript.

Common Positioning Patterns

/* Tooltip below, centered */
.tooltip-bottom {
  position-area: bottom;
  position-try-fallbacks: --top, --right;
}

/* Dropdown menu from button */
.dropdown {
  position-area: bottom span-left;
  width: anchor-size(width);
}

/* Context menu near cursor */
.context-menu {
  position: fixed;
  position-area: center;
}

Scroll-Driven Animations: view-timeline and scroll-timeline

Scroll-driven animations map animation progress to scroll position instead of time. They replace verbose Intersection Observer callbacks and scroll-event listeners.

Scroll Progress Timeline

Link an animation to the document’s scroll progress using scroll():

.progress-bar {
  animation: fill-width linear;
  animation-timeline: scroll();
}

@keyframes fill-width {
  from { width: 0%; }
  to   { width: 100%; }
}

As the user scrolls from top to bottom, the progress bar fills from 0% to 100%.

View Progress Timeline

Use view() to trigger animations based on an element’s visibility in the viewport:

.fade-in {
  animation: fade-up linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(32px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

The element fades in and slides up as it scrolls into view.

Named Timelines for Precise Control

For complex scenarios, name the timeline on a specific element:

.article {
  view-timeline-name: --article;
  view-timeline-axis: block;
}

.progress-indicator {
  animation-timeline: --article;
  animation-range: entry 0% exit 100%;
}

@keyframes indicate-progress {
  from { scale: 0 1; }
  to   { scale: 1 1; }
}

Scroll-Triggered Animations

Chrome 145+ introduces scroll-triggered animations — time-based animations that fire when crossing a scroll threshold:

.hero {
  animation: reveal 0.6s ease-out both;
  animation-trigger: scroll(nearest);
}

@keyframes reveal {
  from {
    opacity: 0;
    clip-path: inset(0 0 100% 0);
  }
  to {
    opacity: 1;
    clip-path: inset(0);
  }
}

The animation plays once when the element scrolls into view, then completes on its own timeline.

@starting-style: Smooth Enter Transitions

Elements added to the DOM or toggled from display: none traditionally could not animate their entry — they appeared instantly. @starting-style defines a pre-entry state that transitions smoothly to the final style.

Basic Enter Animation

.toast {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s, transform 0.3s, display 0.3s allow-discrete;
}

@starting-style {
  .toast {
    opacity: 0;
    transform: translateY(16px);
  }
}

The allow-discrete keyword on transition-behavior permits animating discrete properties like display.

Dialog and Popover Entry

Combine with the Popover API for smooth modal transitions:

dialog {
  opacity: 0;
  scale: 0.95;
  transition: opacity 0.25s, scale 0.25s, display 0.25s allow-discrete;
}

dialog[open] {
  opacity: 1;
  scale: 1;
}

@starting-style {
  dialog[open] {
    opacity: 0;
    scale: 0.95;
  }
}

List Item Staggering

Create staggered entry animations for dynamic lists:

@starting-style {
  li {
    opacity: 0;
    transform: translateX(-8px);
  }
}

li {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 0.3s, transform 0.3s;
}

li:nth-child(1) { transition-delay: 0ms; }
li:nth-child(2) { transition-delay: 50ms; }
li:nth-child(3) { transition-delay: 100ms; }

text-wrap: balance / pretty

These two values of the text-wrap property solve longstanding typography problems without JavaScript.

balance for Headings

text-wrap: balance distributes text evenly across lines, preventing awkward orphans and uneven line lengths:

h1, h2, h3 {
  text-wrap: balance;
  max-inline-size: 50ch;
}

The browser adjusts line breaks so each line has roughly the same number of characters. The algorithm only applies to blocks with six or fewer lines (Chromium) or ten or fewer (Firefox), since the computation becomes expensive for long text.

pretty for Body Text

text-wrap: pretty uses a slower algorithm that minimizes orphans — single words stranded on the last line:

article p, article li {
  text-wrap: pretty;
  max-inline-size: 65ch;
}

Combined Typography Strategy

/* Headings: balanced lines */
h1, h2, h3, .section-title {
  text-wrap: balance;
}

/* Body: orphan-free paragraphs */
.prose p,
.prose li,
.prose blockquote {
  text-wrap: pretty;
}

/* Narrow containers: standard wrap */
.sidebar p {
  text-wrap: wrap;
}

These are progressive enhancements — browsers that do not support them fall back to wrap with no visual breakage.

Subgrid: Aligned Nested Layouts

Subgrid allows a grid item to inherit column or row tracks from its parent grid, solving alignment problems in nested layouts.

Inheriting Parent Columns

.page-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1.5rem;
}

.card {
  grid-column: span 4;
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  gap: inherit;
}

Each card’s header, content, and footer align across the entire row because they share the same row tracks.

Column Subgrid

Subgrid works on columns too:

.sidebar-group {
  grid-column: span 3;
  display: grid;
  grid-template-columns: subgrid;
}

.sidebar-group .label {
  grid-column: 1;
}

.sidebar-group .value {
  grid-column: 2 / -1;
}

Practical Pattern: Aligned Card Grid

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  gap: inherit;
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.card h3 {
  grid-row: 1;
  margin: 0;
}

.card p {
  grid-row: 2;
}

.card .actions {
  grid-row: 3;
  align-self: end;
}

All cards share the same row track sizing — titles, descriptions, and action areas align perfectly regardless of content length.

Browser Support Table (2026)

Feature Chrome Edge Firefox Safari
Container queries 105+ 105+ 110+ 16.0+
Cascade layers 99+ 99+ 97+ 15.4+
CSS nesting 120+ 120+ 117+ 17.2+
:has() selector 105+ 105+ 121+ 15.4+
oklch() 111+ 111+ 113+ 15.4+
color-mix() 111+ 111+ 113+ 16.2+
light-dark() 123+ 123+ 120+ 17.5+
@property 85+ 85+ 128+ 16.4+
Anchor positioning 125+ 125+ 132+ 18.0+
Scroll-driven animations 115+ 115+ 134+
@starting-style 117+ 117+ 129+ 17.5+
text-wrap: balance 114+ 114+ 121+ 17.5+
text-wrap: pretty 117+ 117+ 17.5+
Subgrid 117+ 117+ 71+ 16.0+

All features marked with a version number are stable and widely available. Safari support for scroll-driven animations is in active development and expected by late 2026.

Conclusion

The modern CSS landscape in 2026 offers native solutions for patterns that previously required JavaScript libraries, preprocessors, or complex workarounds. Container queries deliver true component responsiveness. Cascade layers eliminate specificity wars. :has(), anchor positioning, and scroll-driven animations remove dependency on JS for common UI patterns. Modern color with OKLCH, color-mix(), and light-dark() makes design systems more maintainable.

The common thread across all these features: the browser absorbs complexity that used to live in application code. CSS is no longer a declarative styling language — it is a state-aware, context-aware layout and interaction engine.

For more CSS patterns, see Advanced CSS Techniques and Frontend Performance Optimization.

Resources

Comments

👍 Was this article helpful?