CSS Container Queries with container units (cqi, cqw, cqh, cqb) combined with Utopia.fyi fluid scales for component-level responsiveness...
Component-level responsiveness using container queries with Utopia.fyi fluid scales
Container queries extend Utopia's declarative, fluid design approach from the viewport level to the component level. Instead of components responding only to viewport width, they respond to their container's dimensions, enabling truly modular, context-aware design.
Key Insight: Combine Utopia's fluid scales (viewport-based) with container query units (container-based) for layered responsiveness — global scaling + component adaptation.
Fully supported: Chrome 105+, Edge 105+, Firefox 110+, Safari 16+ Baseline: ✅ Widely available since February 2023 Browser Compatibility: 82/100
/* Long form */
.container {
container-name: card;
container-type: inline-size; /* Queries width (horizontal) or height (vertical) */
}
/* Shorthand */
.container {
container: card / inline-size;
}
Container Types:
inline-size - Query inline dimension (width in LTR, height in vertical writing)size - Query both dimensions (use sparingly, affects layout performance)normal - Default, no containmentBest Practice: Use inline-size for most cases — queries container width without layout side effects.
/* Named container */
@container card (inline-size > 400px) {
.card {
display: grid;
grid-template-columns: 150px 1fr;
}
}
/* Unnamed (uses nearest ancestor container) */
@container (width > 500px) {
.component {
display: flex;
gap: var(--space-m);
}
}
/* Range queries (modern, readable) */
@container (400px <= inline-size <= 800px) {
.element {
/* Styles for containers between 400-800px */
}
}
Relative sizing units based on container dimensions, not viewport:
cqw - 1% of container widthcqh - 1% of container heightcqi - 1% of container inline size (recommended)cqb - 1% of container block sizecqmin - Smaller of cqi or cqbcqmax - Larger of cqi or cqbRecommended: Use cqi for international audiences — automatically switches to horizontal or vertical axis based on writing mode.
.card {
/* Responsive padding based on container */
padding: clamp(1rem, 4cqi, 2rem);
/* Fluid typography */
font-size: clamp(1rem, 3cqi, 1.6rem);
/* Dynamic gaps */
gap: clamp(0.5rem, 2cqw, 1.5rem);
}
How This Works:
Layer 1: Viewport-based Utopia fluid scales (global) Layer 2: Container query units (component-level)
:root {
/* Utopia viewport-based type scale */
--step-0: clamp(1rem, 0.9286rem + 0.3571vi, 1.25rem);
--step-1: clamp(1.2rem, 1.1036rem + 0.4821vi, 1.5625rem);
--step-2: clamp(1.44rem, 1.3113rem + 0.6435vi, 1.9531rem);
/* Utopia space scale */
--space-s: clamp(1rem, 0.9286rem + 0.3571vi, 1.25rem);
--space-m: clamp(1.5rem, 1.3929rem + 0.5357vi, 1.875rem);
--space-l: clamp(2rem, 1.8571rem + 0.7143vi, 2.5rem);
}
.card-container {
container: card / inline-size;
}
.card {
/* Base: Utopia fluid scale (viewport-responsive) */
padding: var(--space-m);
font-size: var(--step-0);
/* Additional container-based adjustments */
gap: clamp(var(--space-s), 3cqi, var(--space-m));
}
Why This Works:
.card-container {
container: card / inline-size;
}
.card {
/* Base typography from Utopia */
font-size: var(--step-0); /* 16px → 20px (viewport) */
/* Container-based refinement */
@container (inline-size > 400px) {
font-size: clamp(var(--step-0), 2.5cqi + 0.5rem, var(--step-1));
}
}
.card__title {
/* Viewport-based from Utopia */
font-size: var(--step-2);
/* Container-based enhancement */
@container (inline-size > 600px) {
font-size: clamp(var(--step-2), 4cqi, var(--step-3));
}
}
Pattern:
.card-container {
container: card / inline-size;
}
.card {
/* Base padding from Utopia space scale */
padding: var(--space-s-m); /* 16px → 30px (viewport) */
/* Container-based adjustment */
@container (inline-size > 500px) {
padding: clamp(var(--space-m), 5cqi, var(--space-l));
gap: clamp(var(--space-s), 2cqi, var(--space-m));
}
}
Best Practice: Use Utopia space tokens as min/max bounds for container-based clamp() calculations.
✅ Use Container Queries:
✅ Use Media Queries:
prefers-color-scheme, prefers-reduced-motion)/* Global: Media query for dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1a1a1a;
--color-text: #f5f5f5;
}
}
/* Component: Container query for layout */
@container card (inline-size > 400px) {
.card {
display: grid;
grid-template-columns: 150px 1fr;
}
}
.card-container {
container: card / inline-size;
}
.card {
/* Base: Utopia scales */
padding: var(--space-s);
gap: var(--space-xs);
font-size: var(--step-0);
display: grid;
}
/* Small: Stacked layout */
@container card (inline-size < 400px) {
.card {
grid-template-columns: 1fr;
padding: var(--space-s);
}
.card__image {
aspect-ratio: 16 / 9;
}
}
/* Medium: Horizontal layout */
@container card (inline-size >= 400px) {
.card {
grid-template-columns: 150px 1fr;
padding: clamp(var(--space-s), 3cqi, var(--space-m));
gap: clamp(var(--space-xs), 2cqi, var(--space-s));
}
}
/* Large: Enhanced layout */
@container card (inline-size >= 600px) {
.card {
grid-template-columns: 200px 1fr;
padding: clamp(var(--space-m), 4cqi, var(--space-l));
gap: clamp(var(--space-s), 3cqi, var(--space-m));
}
.card__title {
font-size: clamp(var(--step-1), 3cqi, var(--step-2));
}
.card__image {
aspect-ratio: 3 / 4;
}
}
Why This Works:
.nav-container {
container: nav / inline-size;
}
.nav {
display: flex;
gap: var(--space-s);
padding: var(--space-s);
}
/* Narrow: Vertical stack */
@container nav (inline-size < 600px) {
.nav {
flex-direction: column;
gap: var(--space-2xs);
}
.nav__item {
width: 100%;
}
}
/* Wide: Horizontal with fluid gaps */
@container nav (inline-size >= 600px) {
.nav {
flex-direction: row;
gap: clamp(var(--space-s), 2cqi, var(--space-m));
}
}
.article-container {
container: article / inline-size;
}
.article {
/* Base typography from Utopia */
font-size: var(--step-0);
line-height: 1.6;
padding: var(--space-m);
}
.article h1 {
font-size: var(--step-3);
margin-block-end: var(--space-m);
}
/* Container-based enhancements */
@container article (inline-size > 600px) {
.article {
padding: clamp(var(--space-m), 4cqi, var(--space-l));
max-width: 70ch;
}
.article h1 {
font-size: clamp(var(--step-3), 5cqi, var(--step-4));
margin-block-end: clamp(var(--space-m), 3cqi, var(--space-l));
}
}
@container article (inline-size > 900px) {
.article {
font-size: clamp(var(--step-0), 2cqi + 0.5rem, var(--step-1));
}
.article h1 {
font-size: clamp(var(--step-4), 6cqi, var(--step-5));
}
}
/* Parent uses Utopia grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
gap: var(--space-m-l);
}
/* Each grid item is a container */
.grid-item {
container: item / inline-size;
}
/* Item adapts to its grid cell width */
.grid-item__content {
padding: var(--space-s);
font-size: var(--step-0);
}
@container item (inline-size > 350px) {
.grid-item__content {
padding: clamp(var(--space-s), 3cqi, var(--space-m));
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-s);
}
}
@container item (inline-size > 500px) {
.grid-item__content {
font-size: clamp(var(--step-0), 2.5cqi, var(--step-1));
}
}
Pattern: Grid controls layout, container queries control item internals.
Multiple containers can coexist with independent queries:
.page-container {
container: page / inline-size;
}
.sidebar-container {
container: sidebar / inline-size;
}
.card-container {
container: card / inline-size;
}
/* Page-level query */
@container page (inline-size > 1200px) {
.page {
display: grid;
grid-template-columns: 300px 1fr;
gap: var(--space-l);
}
}
/* Sidebar-level query */
@container sidebar (inline-size > 250px) {
.sidebar-nav {
font-size: var(--step-0);
gap: var(--space-s);
}
}
/* Card-level query (inside sidebar OR main) */
@container card (inline-size > 400px) {
.card {
display: grid;
grid-template-columns: 150px 1fr;
}
}
Why This Works:
/* ⚠️ Can cause accessibility issues */
.component {
font-size: 3cqw; /* No min/max bounds */
}
Problem: When user zooms, container width may not change proportionally, breaking text scaling.
Solution: Always use clamp() with Utopia steps as bounds:
/* ✅ Accessible */
.component {
font-size: clamp(var(--step-0), 2.5cqi, var(--step-1));
}
Testing Checklist:
/* ✅ Good - users retain zoom control */
font-size: clamp(1rem, 2cqi + 0.5rem, 1.5rem);
/* ⚠️ Problematic - reduces user control */
font-size: clamp(0.5rem, 5cqi, 3rem);
Guideline: Keep the central rem value close to 1rem and cqi value low (2-3cqi) to preserve user zoom control.
/* Base styles (all browsers) */
.card {
padding: var(--space-m);
font-size: var(--step-0);
}
/* Enhanced with container queries */
@supports (container-type: inline-size) {
.card-container {
container: card / inline-size;
}
@container card (inline-size > 400px) {
.card {
display: grid;
grid-template-columns: 150px 1fr;
padding: clamp(var(--space-m), 4cqi, var(--space-l));
}
}
}
if (CSS.supports('container-type: inline-size')) {
document.documentElement.classList.add('supports-container-queries');
}
/* Default */
.component {
padding: var(--space-m);
}
/* Enhanced when supported */
.supports-container-queries .component-container {
container: component / inline-size;
}
.supports-container-queries .component {
@container (inline-size > 500px) {
padding: clamp(var(--space-m), 4cqi, var(--space-l));
}
}
utopia-grid-layout for complete system.card-container {
container: card / inline-size;
}
.card {
padding: var(--space-s);
gap: var(--space-xs);
font-size: var(--step-0);
}
@container card (inline-size > 400px) {
.card {
display: grid;
grid-template-columns: auto 1fr;
padding: clamp(var(--space-s), 3cqi, var(--space-m));
}
}
.widget-container {
container: widget / inline-size;
}
.widget {
padding: var(--space-m);
}
.widget__title {
font-size: var(--step-2);
}
@container widget (inline-size < 300px) {
.widget {
padding: var(--space-s);
}
.widget__title {
font-size: var(--step-1);
}
}
@container widget (inline-size > 600px) {
.widget {
padding: clamp(var(--space-m), 4cqi, var(--space-l));
}
.widget__title {
font-size: clamp(var(--step-2), 4cqi, var(--step-3));
}
}
/* Same component, different contexts */
.component-container {
container: component / inline-size;
}
.component {
/* Base: Works everywhere */
padding: var(--space-s);
font-size: var(--step-0);
}
/* In sidebar (narrow) */
@container component (inline-size < 400px) {
.component {
/* Compact vertical layout */
display: flex;
flex-direction: column;
gap: var(--space-2xs);
}
}
/* In main content (wide) */
@container component (inline-size >= 400px) {
.component {
/* Spacious horizontal layout */
display: grid;
grid-template-columns: auto 1fr;
gap: clamp(var(--space-s), 2cqi, var(--space-m));
}
}
Prerequisites:
utopia-fluid-scales first to generate type and space scalesutopia-grid-layout for page-level layouts with fluid gapsWorkflow:
utopia-fluid-scales)utopia-grid-layout)utopia-container-queries)Result: Complete Utopia-aligned responsive system with layered fluidity:
After implementing container queries: