TT Lab

한국어 English 日本語

Get started
Blog

Blog

Web Standards & CSS 2026 — Container Queries / View Transitions / Popover / Anchor Positioning / Scroll-driven Animations Deep Dive

1. The 2026 Web Platform — Standards That Finally Arrived

A decade ago we said "this will soon be standard" while installing polyfills. In 2026 we live in the era where standards are now everyday.

Right now, Chrome 130+, Safari 18+, and Firefox 130+ all stably support:

The answer to "what should I use if I start a new project today?" has finally become simple. Just use the standards. This article walks through the new baseline one chapter at a time.

Not a single line in this article requires a polyfill. Everything targets the latest stable releases of 2026 evergreen browsers.


2. Container Queries — Media Queries for the Component Era

Why Media Queries Are Not Enough

Media queries are viewport-based. But components live inside sidebars, modals, card grids, side panels. The same card shrinks in a sidebar and stretches in main content. The viewport is identical. Only the container width differs.

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

@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
    gap: 1.5rem;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}

Three container-type values

ValueMeaning
inline-sizeQuery inline (horizontal) size only — most common
sizeQuery both axes — performance cost is higher
normalNot a container (default)

size must prevent children from influencing parent height, which has cost. In 99% of cases, inline-size is enough.

Container Query Units

.hero-title {
  font-size: clamp(1.5rem, 5cqi, 3rem);
}

cqi (container query inline-size), cqb (block-size), cqw, cqh, cqmin, cqmax. They are the container-scoped versions of vw / vh.

Real-world Pattern

.product-grid {
  container-type: inline-size;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: 1rem;
}

@container (min-width: 900px) {
  .product-card .image {
    aspect-ratio: 4 / 3;
  }
}

@container (max-width: 600px) {
  .product-card .price {
    font-size: 0.875rem;
  }
}

The same card adapts whether it sits in a modal, sidebar, or main column. Components finally become real components.


3. View Transitions API — Single-page to Cross-document (2024)

Same-document version (2023)

When state changes, the browser produces a smooth transition automatically.

function updateList(newItems) {
  if (!document.startViewTransition) {
    renderList(newItems)
    return
  }
  document.startViewTransition(() => renderList(newItems))
}
::view-transition-old(root) {
  animation: fade-out 200ms ease-out;
}
::view-transition-new(root) {
  animation: fade-in 200ms ease-in;
}

@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in  { from { opacity: 0; } }

Named transitions

Animate specific elements independently:

.product-card .image {
  view-transition-name: product-image;
}

::view-transition-old(product-image),
::view-transition-new(product-image) {
  animation-duration: 400ms;
}

Clicking a product card to navigate to the detail page can produce a native-mobile-app-style hero zoom automatically.

Cross-document version (2024)

This is the real revolution. MPAs (multi-page apps) get smooth page transitions too.

@view-transition {
  navigation: auto;
}

.hero-image {
  view-transition-name: hero;
}

If the previous page's .hero-image and the new page's .hero-image share the same name, the browser transitions between them automatically. No SPA router. No Next.js Link. A plain anchor click is enough.

Why MPA Transitions Matter

One of the biggest reasons we adopted SPAs for the last decade was "smooth page transitions". The cross-document version of View Transitions removes that reason. Next.js, Astro, and SvelteKit all leverage it automatically.

Caveats


4. Popover API (2024) — Native Modals

Why We Always Failed at Modals

The browser does all of it for you. Since 2024.

<button popovertarget="my-popover">Open</button>

<div id="my-popover" popover>
  <h2>Hello</h2>
  <button popovertarget="my-popover" popovertargetaction="hide">Close</button>
</div>

With just this:

Two popover modes

ValueBehavior
auto (default)Closes on ESC, outside click, or when another popover opens
manualOnly closes explicitly — toasts, notifications

Backdrop styling

[popover]::backdrop {
  background: oklch(0% 0 0 / 0.6);
  backdrop-filter: blur(8px);
}

Difference from <dialog>

<dialog> has form integration and a modal/non-modal distinction. Popover is lighter and applies to any element. Generally:


5. Anchor Positioning (Chrome 125, April 2024) — Tooltip/Popover Placement

The Old Nightmare

To use a single tooltip library you installed Floating UI (formerly Popper.js), detected viewport collisions, listened to scroll events, attached a ResizeObserver. It was insane.

Now:

<button id="trigger" style="anchor-name: --my-button">Hover me</button>

<div popover id="tooltip">
  Tooltip content
</div>

<style>
  #tooltip {
    position-anchor: --my-button;
    top: anchor(bottom);
    left: anchor(center);
    translate: -50% 0;
  }
</style>

The browser tracks the trigger element's position automatically. Whether you scroll or resize, the tooltip follows.

position-try-fallbacks — Automatic viewport collision avoidance

#tooltip {
  position-anchor: --my-button;
  position-area: bottom;
  position-try-fallbacks: top, right, left;
}

Default: show below. If space is tight, try top, then right, then left automatically. Without a single line of Floating UI.

position-area — 9-cell shorthand

#tooltip {
  position-area: top span-all;
}

Use top, bottom, left, right, center, span-all, start, end. Keep the 9-cell grid in mind and it becomes intuitive.

What Does Not Work

The Fate of Floating UI

Still valid — advanced collision detection and arrow auto-positioning remain library territory. But 80% of simple tooltips and popovers are now solvable with CSS alone.


6. Scroll-driven Animations (Chrome 115) — Scroll Effects Without JS

Two scroll timelines

TimelineMeaning
scroll()Overall scroll progress of the container
view()Progress of an element crossing the viewport

scroll() — Progress indicator

@keyframes grow-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: oklch(70% 0.2 250);
  transform-origin: left;
  animation: grow-progress linear;
  animation-timeline: scroll();
}

Scrolling fills it in automatically. Not a single line of JavaScript.

view() — Fade-in on viewport entry

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

.card {
  animation: fade-in linear;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

Start when the element enters the bottom of the viewport, end when 40% visible. No Intersection Observer required.

prefers-reduced-motion

@media (prefers-reduced-motion: no-preference) {
  .card {
    animation: fade-in linear;
    animation-timeline: view();
  }
}

Do not forget accessibility.

Performance

Scroll-driven animations run on the compositor thread, unlike JavaScript scroll handlers that block the main thread. 60fps is the baseline; 120fps is achievable.


7. CSS Nesting (2023) — Nest Without Sass

Standard syntax

.card {
  padding: 1rem;
  border-radius: 0.5rem;

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

  & .description {
    color: oklch(50% 0 0);
  }

  &:hover {
    transform: translateY(-2px);
  }

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

Difference from Sass is minor. You need & more often (mandatory in early 2023, relaxed later).

Differences from Sass

Migration

/* Before (Sass) */
.btn {
  &.primary { background: blue; }
  &.danger { background: red; }
}

/* After (native CSS nesting) */
.btn {
  &.primary { background: oklch(60% 0.2 250); }
  &.danger  { background: oklch(60% 0.25 25); }
}

Works almost as-is.


8. :has() — The Parent Selector

"CSS Has No Parent Selector" Is Over

/* Different layout when a card has an image */
.card:has(img) {
  display: grid;
  grid-template-columns: 1fr 2fr;
}

/* Disable submit button when any input is invalid */
form:has(input:invalid) button[type="submit"] {
  opacity: 0.5;
  pointer-events: none;
}

/* Dark mode unless the html has a "light" class */
html:not(.light) {
  color-scheme: dark;
}

Real-world patterns

1) Parent-child joint condition

.dropdown:has(:focus-visible) {
  outline: 2px solid currentColor;
}

2) Sibling-based styling

label:has(+ input:required)::after {
  content: ' *';
  color: oklch(60% 0.25 25);
}

3) Empty state handling

.list:not(:has(.list-item)) {
  display: none;
}

Performance

:has() is not brute force — it has invalidation tracking. Practical even on very large DOMs.


9. color-mix() / light-dark() — Color Arithmetic

color-mix() — Mix two colors

.btn {
  background: color-mix(in oklch, blue, white 20%);
}

.btn:hover {
  background: color-mix(in oklch, blue, white 30%);
}

Specify a color space with in oklch / in lab / in srgb / in hsl. Generally oklch is the most natural (a perceptually uniform space).

Combining with Design Tokens

:root {
  --brand: oklch(60% 0.2 250);
}

.btn {
  background: var(--brand);
}
.btn:hover {
  background: color-mix(in oklch, var(--brand), white 15%);
}
.btn:active {
  background: color-mix(in oklch, var(--brand), black 15%);
}

No need to predefine every color variant — derive everything from one brand color.

light-dark() — Dark mode in one line

:root {
  color-scheme: light dark;
  --bg: light-dark(white, oklch(15% 0 0));
  --fg: light-dark(oklch(15% 0 0), white);
}

body {
  background: var(--bg);
  color: var(--fg);
}

No need for two prefers-color-scheme blocks. But you must declare color-scheme: light dark for it to work.

Why OKLCH Is Becoming Standard

#hex or rgb() lives in the RGB space, which does not match human perception. oklch(60% 0.2 250) gives:

Color interpolation is natural, lightness changes are intuitive. In 2026, design systems are de-facto OKLCH-first.


10. @scope / @layer — Encapsulation and Cascade

@scope — Real CSS scoping

@scope (.card) to (.card-content) {
  h2 {
    font-size: 1.25rem;
  }
}

Applies inside .card but not below .card-content. A scope with a lower boundary.

@scope (.dark-section) {
  a {
    color: white;
  }
}

Only anchors inside .dark-section become white.

@layer — Cascade layers

@layer reset, base, components, utilities;

@layer reset {
  * { margin: 0; padding: 0; }
}

@layer base {
  body { font-family: system-ui; }
}

@layer components {
  .btn { padding: 0.5rem 1rem; }
}

@layer utilities {
  .text-center { text-align: center; }
}

The later the layer, the higher the priority. Independent of specificity.

Why it matters: Tailwind/UnoCSS utility-first triggered specificity wars. By placing utilities in @layer utilities last, they always win without specificity hacks.

Cascade Layers + Tailwind 4

Tailwind 4 uses @layer internally. Place your CSS in a separate layer and priority resolves cleanly.


11. Subgrid — Finally Universal Support

The problem

<ul class="cards">
  <li class="card">
    <h2>Short title</h2>
    <p>Description</p>
    <button>Buy</button>
  </li>
  <li class="card">
    <h2>A much longer title that wraps to two lines</h2>
    <p>Description</p>
    <button>Buy</button>
  </li>
</ul>

We want titles, descriptions, and buttons across cards to align horizontally. Children need to inherit the parent grid's grid lines.

Subgrid solution

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1rem;
}

.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
}

grid-template-rows: subgrid inherits the parent's row structure. Titles align to the same height automatically.

What Subgrid Solved


12. Speculation Rules — Prerender Future Pages

What it is

Pre-render the links the user is likely to click. Stronger than prefetch — DOM, JavaScript execution, all of it.

<script type="speculationrules">
{
  "prerender": [
    {
      "where": { "href_matches": "/articles/*" },
      "eagerness": "moderate"
    }
  ]
}
</script>

For every link starting with /articles/, when the user shows intent signals (hover, touch start), the page is prerendered in advance.

eagerness levels

ValueMeaning
immediateRight away — use with care
eagerWhen the link enters the viewport
moderate200ms hover or mousedown
conservativeMousedown only

Impact

Next-page LCP drops to a few milliseconds. From the user's perspective: "the next page is already there when I click."

Relationship to Next.js

Next.js App Router prefetches automatically (code and data). Speculation Rules prerender goes further — it pre-renders the DOM as well. Opt-in support exists in Next.js 15+.


13. WebGPU / WebTransport — Stable

WebGPU

You can now run LLM inference (WebLLM), image processing, and simulations on GPU acceleration inside the browser. The era of running ChatGPT-like local models in a browser tab has truly arrived.

WebTransport

const transport = new WebTransport('https://example.com/wt')
await transport.ready
const stream = await transport.createBidirectionalStream()
const writer = stream.writable.getWriter()
await writer.write(new TextEncoder().encode('hello'))

Other stabilized APIs


14. Korean / Japanese Web Standards Content — Naver D2, html5.jp, mizchi

Korean

Japanese

English-speaking world

Japanese standards quirk

W3C Japan node activity is very active. Unlike Korea, Japan has a strong spec-translation culture, with many articles chewing over the spec sentence by sentence. If you want to understand the "why" of a standard, html5.jp is recommended reading.


15. Migration Strategy — Where to Start

Priorities

Tier 1 — Adopt now (universal support)

Tier 2 — Gradual adoption (mostly supported, graceful fallback)

Tier 3 — Opt-in (specific scenarios)

Progressive enhancement pattern

.tooltip {
  /* fallback: basic absolute positioning */
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
}

@supports (position-anchor: --x) {
  .tooltip {
    position-anchor: --trigger;
    top: anchor(bottom);
    left: anchor(center);
    transform: translateX(-50%);
  }
}

Branch via @supports. Enhance if the new feature is there, fall back otherwise.

Design system migration checklist

  1. OKLCH color migration — convert design tokens to color-mix(in oklch, ...) first
  2. Modal library to Popover API — dialog for forms, popover otherwise
  3. Floating UI to Anchor Positioning — start with simple tooltips
  4. Intersection Observer to Scroll-driven Animations — fade-ins first
  5. Tailwind/Sass variables to @layer organization — end of priority conflicts
  6. Responsive components to Container Queries — replace media queries
  7. SPA router transitions to View Transitions (cross-document) — gradual adoption

Common pitfalls

Conclusion — The 2026 baseline

Every feature in this article is a 2026 evergreen-browser baseline. Not "advanced CSS" — just CSS. Before installing one more library, ask: "Isn't this in the standard?" The answer is usually yes.

In 2026, betting on standards is the winning bet.


References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment