Type something to search...
How to Use CSS Container Queries for Responsive Typography?

How to Use CSS Container Queries for Responsive Typography?

You build a product card with a confident 1.75rem heading. It looks great in the three-column grid on the homepage. Then someone drops the same card into a 280px sidebar, and the heading wraps into four cramped lines with a single word on each. Your media queries cannot help, because the viewport is still 1440px wide. The card does not care how wide the window is. It cares how wide its own slot is.

That is the exact problem CSS container queries solve. This article explains how container queries work, how to use container query units like cqi for type sizing, how to combine them with clamp() so text still respects browser zoom, and which patterns hold up in real component libraries.

What Are Container Queries?

A container query lets an element change its styles based on the size of an ancestor container rather than the size of the viewport. You mark an element as a query container, then write @container rules that apply to its descendants when that container meets a condition.

For typography, that means a heading inside a card can be large when the card is wide and smaller when the card is narrow, regardless of where the card sits on the page.

ApproachResponds toBest for
Media queriesViewport width, height, preferencesPage-level layout, global type scale
Viewport units (vw)Viewport widthHero headlines that span the full page
Container queriesWidth of a specific ancestorReusable components: cards, widgets, sidebars
Container units (cqi)Width of the nearest containerSmoothly scaling type inside a component

Container size queries and container query units have been supported in Chrome, Edge, Safari, and Firefox since early 2023, so you can use them in production without a polyfill.

Why Viewport-Based Type Breaks in Components

Most responsive type systems are built on the viewport. You set h2 to 1.5rem on mobile and 2rem above 768px, or you use a fluid formula with vw. That works for page-level content, where the text column grows and shrinks with the window.

It fails for components because one component can live in many contexts at once:

  • A card in a four-column grid on a wide screen may be narrower than the same card full-width on a phone.
  • A newsletter signup block can appear in a sidebar, a footer, and a full-width banner on the same page.
  • A CMS editor can drop any block into any column layout without your knowledge.

If the type is tied to the viewport, the widest viewport produces the largest heading even when that heading is crammed into a 250px column. Container queries move the decision to the component itself.

Setting Up a Query Container

You create a container with the container-type property. For typography you almost always want inline-size, which lets descendants query the container's width (its inline dimension in horizontal writing modes).

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

.card h2 {
  font-size: 1.25rem;
  line-height: 1.25;
}

@container (min-width: 28rem) {
  .card h2 {
    font-size: 1.75rem;
    line-height: 1.15;
  }
}

A few rules matter here:

  1. A container cannot style itself with its own query. The @container rule targets descendants. If you need to change the card's padding based on its width, wrap the card's contents in an inner element or make the card's parent the container.
  2. inline-size applies size containment on the inline axis. The container's width can no longer depend on its children. In a normal block layout this changes nothing, but a container inside a shrink-to-fit context (a flex item with no basis, an inline-block, a float) can collapse to zero width. Give those containers an explicit width or flex: 1.
  3. Use size only when you need height queries. container-type: size contains both axes, which means the element needs an explicit height. That is rarely what you want for text.

Naming Containers

When components nest, the nearest container wins by default. Name your containers to target a specific one:

.card {
  container: card / inline-size;
}

.sidebar {
  container: sidebar / inline-size;
}

@container card (min-width: 30rem) {
  .card__title {
    font-size: 2rem;
  }
}

@container sidebar (max-width: 20rem) {
  .card__title {
    font-size: 1.125rem;
  }
}

The container shorthand takes the name first, then a slash, then the type. Names are plain identifiers and an element can have more than one, separated by spaces.

Container Query Units for Fluid Type

Breakpoint-style container queries give you steps. Container query units give you a smooth scale. They work like viewport units, but relative to the nearest query container:

UnitEquals
cqw1% of the container's width
cqh1% of the container's height
cqi1% of the container's inline size
cqb1% of the container's block size
cqminThe smaller of cqi and cqb
cqmaxThe larger of cqi and cqb

Prefer cqi over cqw. It follows the writing mode, so the same rule works for vertical text in Japanese or Mongolian layouts.

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

.card__title {
  font-size: 6cqi; /* 6% of the card's width */
}

On a 300px card that heading is 18px. On a 600px card it is 36px. If no ancestor is a query container, the units fall back to the small viewport units (svw, svh), which is usually not what you meant, so make sure a container exists.

Combine cqi with clamp() and rem

A heading set only in cqi has two serious problems. It has no floor or ceiling, so it can become unreadably small or absurdly large. And it ignores the user's font size preference and browser zoom, because a container's width in CSS pixels does not change when the user bumps their default font size. That is a direct risk under WCAG 1.4.4 Resize Text, which requires text to scale to 200% without loss of content.

The fix is the same one used in fluid typography with CSS clamp(): bound the value, and mix in a rem component so the preferred size always grows with user settings.

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

.card__title {
  /* min 1.125rem, max 2.25rem, scales with the card in between */
  font-size: clamp(1.125rem, 0.75rem + 4cqi, 2.25rem);
  line-height: 1.15;
}

.card__body {
  font-size: clamp(1rem, 0.9rem + 0.5cqi, 1.125rem);
  line-height: 1.55;
}

The 0.75rem + part matters. Because part of the preferred value is in rem, a user who sets their browser default to 20px gets a proportionally larger heading even at the same card width. The rem minimum and maximum also scale with the user's settings.

Working Out the Slope

If you want the heading to be 1.125rem (18px) at a 300px card and 2.25rem (36px) at a 700px card, solve the linear equation:

  1. Slope = (36 − 18) / (700 − 300) = 0.045, which is 4.5cqi.
  2. Intercept = 18 − (0.045 × 300) = 4.5px, which is 0.28125rem.
  3. Result: clamp(1.125rem, 0.28125rem + 4.5cqi, 2.25rem).

Keep the slope moderate for body copy. Body text that swings more than a couple of pixels across container sizes is distracting, and the best font size for body text is usually best held near 1rem to 1.125rem everywhere.

Practical Patterns

A Card That Adapts Its Hierarchy

Size alone is not the whole story. In a narrow card you might also reduce the gap between title and meta, drop the eyebrow text, and tighten line height.

.card {
  container: card / inline-size;
}

.card__eyebrow {
  display: none;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.card__title {
  font-size: clamp(1.125rem, 0.5rem + 4cqi, 2rem);
  line-height: 1.2;
  text-wrap: balance;
}

@container card (min-width: 24rem) {
  .card__eyebrow {
    display: block;
  }

  .card__title {
    line-height: 1.1;
  }
}

The text-wrap: balance declaration keeps short headings from ending in a lonely word, which helps a lot in narrow containers.

Constraining Line Length Inside a Container

Container queries pair well with the ch unit. When an article module gets wide, cap its measure so lines stay within 45–75 characters, the range covered in our guide to ideal line length.

.prose-block {
  container-type: inline-size;
}

.prose-block p {
  max-width: 68ch;
}

@container (min-width: 50rem) {
  .prose-block p {
    font-size: 1.125rem;
  }
}

Container Queries in Tailwind CSS v4

Tailwind v4 includes container queries in core. Mark the parent with @container and use size variants like @md: on children. Named containers use a slash.

<article class="@container/card rounded-lg p-4">
  <h2 class="text-lg leading-tight @md/card:text-2xl @xl/card:text-3xl">
    Quarterly report
  </h2>
  <p class="text-base @md/card:text-lg">Revenue grew 12% over the previous quarter.</p>
</article>

You can define custom container sizes in your CSS-first config:

@import "tailwindcss";

@theme {
  --container-card-sm: 20rem;
  --container-card-lg: 36rem;
}

That generates @card-sm: and @card-lg: variants.

Container Queries vs. Media Queries for Type

Container queries do not replace media queries. Use each for what it measures.

  • Global type scale and root font size: media queries or a viewport-based fluid scale. The page body, article columns, and site header respond to the viewport.
  • Component type: container queries. Anything reusable that can appear in more than one column width.
  • User preferences: media queries such as prefers-reduced-motion and prefers-contrast. Containers know nothing about user settings.

A clean system defines tokens at the root, then lets components pick from those tokens using container conditions. If you already manage type with CSS custom properties, you can switch a token inside a container query:

:root {
  --title-size: 1.25rem;
}

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

/* The query targets a child of the container, never the container itself */
@container (min-width: 32rem) {
  .card__inner {
    --title-size: 1.75rem;
  }
}

.card__title {
  font-size: var(--title-size);
}

Notice that the custom property is overridden on .card__inner, not on .card. Setting it on .card inside the card's own query does nothing, because a container cannot respond to its own size. This is the most common container query bug you will hit, so it is worth remembering.

Common Mistakes

  1. Forgetting the container. Without container-type on an ancestor, @container rules never match and cqi resolves against the viewport.
  2. Querying the element you are styling. Containers style descendants only.
  3. Containers inside flex or grid items that shrink to content. Size containment makes them collapse. Set a width, flex: 1, or min-width: 0 with a defined track.
  4. Pure cqi font sizes. They ignore zoom and user font settings. Always include a rem term and clamp.
  5. Too many breakpoints. Two or three container breakpoints per component are plenty. More than that usually means the component is doing too much.
  6. Using container-type: size for text. It requires an explicit height and causes overflow surprises.

Testing Your Container-Based Type

  • Drop the component into the narrowest and widest slots it will ever occupy and check that headings wrap to no more than three lines.
  • Set the browser default font size to 20px or 24px and confirm text grows.
  • Zoom to 200% and confirm nothing clips or overlaps, as WCAG 1.4.4 requires.
  • In Chrome DevTools, the Elements panel shows a container badge next to query containers. Click it to highlight the container and see which queries apply.

Container Queries Typography FAQ

Yes. Size container queries and container query units are supported in current versions of Chrome, Edge, Firefox, and Safari, and have been since 2023. Style queries for custom properties have narrower support, so treat them as a progressive enhancement.

Use cqi. It measures the container's inline size, which is the width in horizontal writing modes and the height in vertical ones. That makes your rules work correctly if the component is ever used with vertical text.

Only when used alone. A font size set purely in cqi does not respond to the user's default font size. Combine it with a rem term inside clamp(), and use rem for the minimum and maximum, so text still scales with user preferences and browser zoom.

The most common causes are a missing container-type on the ancestor, trying to style the container element itself from its own query, or a container that has collapsed to zero width because it sits in a shrink-to-fit layout. Check the container badge in DevTools to confirm the container's size.

No. Media queries still handle page-level layout, the root type scale, and user preference features like reduced motion. Container queries are for components whose size depends on where they are placed rather than on the viewport.

Not in any measurable way for typical pages. Browsers evaluate container queries during layout, and size containment can actually limit how much of the page needs relayout. Thousands of nested containers could add cost, but normal component libraries are fine.

Conclusion

Container queries fix a long-standing gap in responsive typography: components that look right in one column and wrong in another. By making the component the query container, you let its headings and body text respond to the space they actually have, which is what readers see.

The reliable recipe is short. Put container-type: inline-size on the component wrapper, size type with clamp() using a rem floor, a rem ceiling, and a rem + cqi preferred value, and keep breakpoints few. Test in the narrowest and widest slots, at larger default font sizes, and at 200% zoom. Do that and your components will carry readable type wherever editors put them.

Here are some useful references for going deeper on container queries and responsive typography:

  1. MDN Web Docs: CSS container queries — syntax, container types, and container query length units.
  2. web.dev: Container queries land in stable browsers — overview of support and practical examples.
  3. W3C: WCAG 2.2 Understanding 1.4.4 Resize Text — why text must scale to 200% without loss of content.
  4. Tailwind CSS Docs: Responsive design: container queries — the @container utility and named container variants in v4.
  5. CSS-Tricks: CSS Container Queries guide — a broad reference with patterns and gotchas.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading