BLOG · UPDATED 2026-04-17
CSS Container Queries + View Transitions: The 2026 Complete Guide
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
- Container size queries
- Container style queries
- Container query units (cqw, cqh)
- View Transitions basics
- Cross-document View Transitions
- Naming and shared-element transitions
- Scroll-driven animations
- Patterns replacing media queries + FLIP
- Debugging what you write
- What you can finally drop from your CSS
- FAQ
The 2026 Baseline
Interop 2026 closed the last gaps. What works everywhere:
| Feature | Chrome/Edge | Safari | Firefox | Baseline |
|---|---|---|---|---|
| @container size queries | 105+ | 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 Transitions | 111+ | 18+ | 133+ | Yes (2025) |
| Cross-document View Transitions | 126+ | 18.2+ | In progress | Newly available |
| Scroll-driven animations | 115+ | 26+ | In progress | Newly available |
| :has() pseudo-class | 105+ | 15.4+ | 121+ | Yes (2023) |
| CSS nesting | 120+ | 16.5+ | 117+ | Yes (2023) |
| Anchor positioning | 125+ | In progress | In progress | No (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:
- Declare a container on the parent:
container-type: inline-size. - Optionally name it:
container-name: card. - 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.pushStatedoes 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-heroon the currently-active hero element only. - Group siblings: give a common
view-transition-classto 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
@containerrules 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
- MDN: CSS Container Queries.
- MDN: View Transition API.
- web.dev Interop 2026 for the full feature rollout schedule.
- Can I Use: Container Queries for up-to-date browser support.
- Core Web Vitals 2026 guide for the performance context.
- INP 150ms fix playbook for the framework code that complements this.
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.