Skip to content

Modern CSS in 2026: container queries, :has() and anchor positioning

Tuan Nguyen Duc Anh
Published date:
Edit this post

For years, the answer to “how do I do X in CSS?” was “use JavaScript”. In 2026 that answer no longer applies in most cases. Three features in particular redrew the design map in the browser.

Table of contents

Open Table of contents

Container Queries: respond to the container, not the screen

Media queries respond to the viewport width. The problem: a component can live in a narrow column or a wide one depending on the parent layout. Container queries solve this.

/* Declare the container */
.card-wrapper {
  container-type: inline-size; 
  container-name: card;
}

/* The component responds to its container */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }

  .card__image {
    grid-row: 1 / 3;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}card.css
<!-- The same component works in any context -->
<aside class="card-wrapper" style="width: 300px">
  <article class="card">...</article>
  <!-- vertical layout -->
</aside>

<main class="card-wrapper" style="width: 700px">
  <article class="card">...</article>
  <!-- horizontal layout -->
</main>card.html

Container query units

Queries also expose container-relative units:

.card__title {
  font-size: clamp(1rem, 4cqi, 2rem); /* cqi = container query inline size */
}typography.css

The :has() pseudoclass — the parent selector we always wanted

:has() selects an element based on its descendants. It’s the “parent selector” that CSS denied for decades.

/* Form with empty required field */
form:has(input:required:invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* Card containing an image: different layout */
.card:has(img) {
  display: grid;
  grid-template-columns: 150px 1fr;
}

.card:not(:has(img)) {
  padding: 1.5rem;
}

/* Navbar with open menu: disable scroll on body */
body:has(.nav-menu[aria-expanded="true"]) {
  overflow: hidden;
}styles.css

:has() has support in all modern browsers since 2023. You can use it in production today without polyfills.

Anchor Positioning: tooltips and popovers without JavaScript

Before Anchor Positioning, placing a tooltip relative to its trigger required calculating positions with JavaScript. Not anymore:

/* Declare the anchor */
.btn-trigger {
  anchor-name: --my-button; 
}

/* Position the tooltip relative to the anchor */
.tooltip {
  position: absolute;
  position-anchor: --my-button; 
  bottom: calc(anchor(top) + 8px); 
  left: anchor(center); 
  transform: translateX(-50%);

  /* Flip automatically if it doesn't fit */
  position-try-fallbacks: flip-block; 
}tooltip.css
<button class="btn-trigger" popovertarget="tip">Hover me</button>
<div id="tip" class="tooltip" popover>
  This tooltip positions itself, without JS.
</div>tooltip.html

position-try-fallbacks: declarative collision logic

.tooltip {
  position-try-fallbacks:
    flip-block,
    /* try top if it doesn't fit below */ flip-inline,
    /* try left if it doesn't fit right */ flip-start; /* combine both */
}tooltip.css

What about support?

FeatureChromeFirefoxSafari
Container Queries105+ ✓110+ ✓16+ ✓
:has()105+ ✓121+ ✓15.4+ ✓
Anchor Positioning125+ ✓131+ ✓18+ ✓

In 2026, with the current browser distribution, you can use all three in production for most projects. Consider polyfills only if your audience includes very old browsers.

Today’s CSS is declarative and expressive

The silent revolution of CSS was not Grid or Flexbox. It was the mindset change: the browser reasons about constraints, you declare the desired result. Container queries, :has() and anchor positioning are the culmination of that paradigm.

Previous
Docker Compose in 2026: best practices that actually matter
Next
React 19: useActionState, useOptimistic, and the end of manual loading states