Skip to content

BLOG · UPDATED 2026-04-17

CSS Container Queries + View Transitions: The 2026 Complete Guide

April 17, 2026 · 20 min read · By FastTool Editors

Two features hit production-ready in 2026 that change how we write CSS: container queries graduated to proper Baseline with Firefox catching up on style queries, and cross-document View Transitions shipped in Safari 18.2, bringing the three-browser consensus that finally makes them usable in multi-page apps. Both have existed for years. Both are still under-used because the tutorials haven't caught up. Here's a working guide for shipping them now.

Table of contents

The 2026 Baseline

Interop 2026 closed the last gaps. What works everywhere:

Feature Chrome/Edge Safari Firefox Baseline
@container size queries105+16+110+Yes (widely available)
@container style queries (custom props)111+18+128+Yes as of Apr 2026
Container query units (cqw, cqi)105+16+110+Yes
Same-document View Transitions111+18+133+Yes (2025)
Cross-document View Transitions126+18.2+In progressNewly available
Scroll-driven animations115+26+In progressNewly available
:has() pseudo-class105+15.4+121+Yes (2023)
CSS nesting120+16.5+117+Yes (2023)
Anchor positioning125+In progressIn progressNo (partial)

Anything marked "Yes" or "widely available" is safe to ship without a polyfill. Anchor positioning is still the laggard; progressive enhancement is the right posture there.

Container Size Queries

A component that doesn't know the viewport size can still style itself based on its own dimensions. The mechanics:

  1. Declare a container on the parent: container-type: inline-size.
  2. Optionally name it: container-name: card.
  3. Query the container from any descendant: @container (min-width: 400px) { ... }.
/* The container parent */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

/* The component responds to its own container */
.card {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}

@container card (min-width: 400px) {
  .card {
    grid-template-columns: 180px 1fr;
  }
}

@container card (min-width: 700px) {
  .card {
    grid-template-columns: 240px 1fr 140px;
  }
}

The same component works in a sidebar (narrow), a main column (medium), or a landing page hero (wide). Media queries can't do that because they only know the viewport.

Container queries are the first CSS feature that makes truly reusable components possible. A card shouldn't care what page it's on; it should care how much room it was given. That's the whole shift.

The inline-size vs size gotcha

container-type: size creates both inline-size and block-size containment. This causes layout bugs if the container is flex or grid inside a flow layout. Use inline-size for 95% of cases unless you specifically need to query container height.

Container Style Queries

Style queries respond to CSS custom properties on the container. This lets a parent switch a theme without class manipulation:

.theme-wrapper {
  --tone: brand;
}

.theme-wrapper.alert {
  --tone: warning;
}

.badge {
  background: hsl(220 50% 90%);
  color: hsl(220 80% 20%);
}

@container style(--tone: warning) {
  .badge {
    background: hsl(35 80% 90%);
    color: hsl(35 80% 20%);
  }
}

@container style(--tone: danger) {
  .badge {
    background: hsl(0 80% 92%);
    color: hsl(0 70% 30%);
  }
}

Under the old approach you'd write .theme-warning .badge selectors with specificity to manage. Style queries separate concerns: the parent sets the tone, the component reads it. No cross-cutting selectors.

Container Query Units (cqw, cqh)

Container units measure against the query container instead of the viewport. Use them for fluid typography or proportional sizing inside reusable components.

.hero-wrapper {
  container-type: inline-size;
}

.hero h1 {
  /* Fluid heading that scales with container, not window */
  font-size: clamp(1.5rem, 6cqw, 3.5rem);
  line-height: 1.1;
}

.hero p {
  font-size: clamp(1rem, 2.5cqw, 1.4rem);
  max-width: 60ch;
}

Units available: cqw (1% of container inline-size), cqh, cqi (inline), cqb (block), cqmin, cqmax.

If you write a lot of fluid typography, our unit converter and rem/px converter help translate between fixed and relative units during design handoff.

View Transitions Basics

Same-document View Transitions animate between two DOM states. Call document.startViewTransition, mutate the DOM inside the callback, and the browser cross-fades the old and new versions.

// Toggle a theme with animation
themeButton.addEventListener('click', () => {
  if (!document.startViewTransition) {
    document.documentElement.classList.toggle('dark');
    return;
  }

  document.startViewTransition(() => {
    document.documentElement.classList.toggle('dark');
  });
});

By default the browser cross-fades the whole document. Customize via CSS:

::view-transition-old(root) {
  animation: fade-out 0.25s ease;
}
::view-transition-new(root) {
  animation: fade-in 0.35s ease 0.05s both;
}
@keyframes fade-out {
  to { opacity: 0; }
}
@keyframes fade-in {
  from { opacity: 0; transform: translateY(8px); }
}

Shared element transitions

Give the same view-transition-name to elements on both sides of the state change. The browser animates between them as a morph.

/* Grid card thumbnail */
.card img.thumb {
  view-transition-name: cover-img;
}

/* Detail view hero image */
.detail .hero-img {
  view-transition-name: cover-img;
}

When you navigate from the grid to the detail inside a startViewTransition, the browser animates the image from its grid position and size to its detail position and size. FLIP math, done by the browser.

Cross-Document View Transitions

Cross-document transitions let a full-page navigation animate instead of hard-cutting. Requires opt-in via CSS:

/* Both pages */
@view-transition {
  navigation: auto;
}

/* Optional: match elements across pages */
.product-hero img {
  view-transition-name: product-image;
}

Any same-origin navigation now transitions by default. With view-transition-name matching on both pages, shared elements morph smoothly. This is the missing link for MPAs that want SPA-feel without SPA complexity.

Restrictions

  • Only same-origin navigations. Cross-origin redirects break the transition.
  • User-initiated navigation only (clicks, form submits). JavaScript-only location changes work too; direct history.pushState does not trigger cross-doc transitions.
  • prefers-reduced-motion users see the transition disabled automatically by the browser.

Naming and Shared-Element Transitions

The naming strategy matters. A name must be unique inside a given transition; if two elements have the same name in the same snapshot, the transition fails silently.

Patterns we use:

  • Unique per-item: view-transition-name: product-{{id}} (via inline style). Each card gets its own name.
  • Shared hero: view-transition-name: page-hero on the currently-active hero element only.
  • Group siblings: give a common view-transition-class to elements that should share animation timing.
/* Stagger children with view-transition-class */
.list-item {
  view-transition-class: list-stagger;
}

::view-transition-group(.list-stagger) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

Scroll-Driven Animations

CSS animations driven by scroll position rather than time. Use for progress bars, parallax, reveal-on-scroll effects without JavaScript libraries.

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

.progress-bar {
  animation: grow linear;
  animation-timeline: scroll(root);
  transform-origin: left;
  height: 4px;
  background: hsl(200 80% 50%);
}

Or tie animation to when an element enters the viewport:

.reveal {
  opacity: 0;
  transform: translateY(20px);
  animation: pop 0.6s forwards;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

@keyframes pop {
  to { opacity: 1; transform: translateY(0); }
}

Firefox support is still in progress as of April 2026. Feature-detect and fall back to an IntersectionObserver-based polyfill for non-supporting browsers.

Patterns Replacing Media Queries + FLIP

Before (media queries everywhere)

.card { display: block; }
@media (min-width: 768px) { .card { display: flex; } }
@media (min-width: 1024px) { .card.in-sidebar { display: block; } }
@media (min-width: 1440px) { .card.in-hero { flex-direction: row; gap: 2rem; } }

Brittle. Each variant knows about its placement. Rearranging the layout breaks the styles.

After (container queries)

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

.card { display: block; }
@container (min-width: 400px) { .card { display: flex; } }
@container (min-width: 700px) { .card { gap: 2rem; } }

Same styles work in sidebar, main column, hero, and modal. Component is context-agnostic.

Before (FLIP for grid-to-detail)

// 60 lines of first/last position capture,
// invert transforms, play animation, manual cleanup.
const rect1 = el.getBoundingClientRect();
navigateToDetail();
const rect2 = el.getBoundingClientRect();
el.animate([...], {...});

After (View Transitions)

document.startViewTransition(() => navigateToDetail());
// CSS:
.card-img, .detail-hero { view-transition-name: hero; }

Two lines. The browser does the math. Works for complex cases FLIP struggled with: clipped elements, fragmented text, transforms mid-animation.

Debugging What You Write

Chrome DevTools added dedicated panels:

  • Animations panel captures view transitions; you can slow them down and scrub frames.
  • Styles panel shows which @container rules matched and which didn't.
  • Layout panel visualizes container-type boundaries.

For CSS that generates unusual output, our CSS minifier and CSS validator catch syntax errors before you ship. When debugging complex gradients and animations, our CSS gradient generator and cubic-bezier generator speed up iteration.

What You Can Finally Drop From Your CSS

Audit your stylesheet and delete these now that baseline is here:

  • ResizeObserver-based component resizing. Container queries handle it natively. Deleted: hundreds of lines of JS in most design systems.
  • FLIP utility libraries. View Transitions replace 90% of use cases. Keep FLIP for frame-by-frame custom timing.
  • IntersectionObserver for reveal animations. animation-timeline: view() does it in CSS.
  • clamp() + viewport unit hacks for component typography. Switch to container units (cqw).
  • JS class toggles for "mobile view" vs "desktop view" at the component level. Container queries express the same logic declaratively.
  • Theme-switching class cascades. Style queries let you define theme at the container level without class collision.

A typical design system can shed 200-500 lines of JavaScript by adopting these features in 2026. Less code, fewer bugs, better performance (because main-thread resize observers are INP killers, discussed in our Core Web Vitals INP guide).

Frequently Asked Questions

Are container queries production-ready?

Yes. Size queries have been stable since 2023. Style queries hit full baseline in 2026 with Firefox 128. If you support the three evergreen browsers at their current major versions, no polyfill needed.

Do container queries work with Tailwind?

Yes, via the official @tailwindcss/container-queries plugin. Shipped in Tailwind 4.0. Syntax is @container md:flex etc.

Can I use View Transitions in an SPA framework?

All major frameworks added support: Next.js 15, Remix/React Router, Nuxt 4, SvelteKit 2, and Astro 4 expose navigation hooks that wrap in startViewTransition. Manual integration is a dozen lines regardless.

What about accessibility?

Both features honor prefers-reduced-motion. View Transitions automatically disable when users have reduced motion preference. For container queries there's nothing a11y-specific to consider; they're layout only.

Do View Transitions affect Core Web Vitals?

Properly used, no. The browser runs the transition off the main thread after the callback completes. Avoid synchronous work inside startViewTransition callbacks; that's where regressions come from.

Can I nest containers?

Yes. Each container creates its own query scope. Named containers (container-name) let you target a specific ancestor from deeply nested descendants.

Further Reading

Container queries and View Transitions were niche curiosities in 2023 and are table stakes in 2026. Teams still writing media-query-only components or hand-rolling FLIP animations are leaving both code quality and performance on the table. Convert one component this week. Write one shared-element transition next week. Drop the libraries that these features replace. Your CSS gets smaller and your UX gets smoother, and for once those aren't in tension.