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
- MDN CSS Container Queries
- MDN Cascade Layers
- MDN :has() Selector
- MDN CSS Nesting
- MDN oklch()
- MDN color-mix()
- MDN light-dark()
- MDN @property
- MDN Anchor Positioning
- MDN Scroll-driven Animations
- MDN @starting-style
- Can I Use: CSS Features
- web.dev: CSS Wrapped 2025
Comments