Container Queries

Axiom does not use viewport media queries. Every responsive utility responds to the width of the nearest container, not the screen. This is a deliberate departure from the Tailwind convention, and it changes how you think about responsive design.

How It Works

Apply .container to any element to make it a containment context. Responsive prefixes inside that element will respond to its width:

Prefix Condition Meaning
(none) Always Default styles, always active
md: Container ≤ 900px Container is medium or smaller
sm: Container ≤ 600px Container is small

The direction is max-width, not min-width. You write your default styles for the full-size layout, then use md: and sm: to simplify as space shrinks. There is no "mobile-first" or "desktop-first" ideology here. Your defaults are whatever makes sense for the component at full width.

Why Not Viewport Queries

Viewport breakpoints answer the wrong question. They ask "how wide is the screen?" when what you actually need to know is "how much space does this component have?"

Consider a card grid. With viewport queries, you decide: at 768px, stack to two columns. But what if that grid is inside a sidebar? It is already narrow at 768px, and your breakpoint fires too late. What if it is in a full-width modal? It might have plenty of room, but the viewport query stacks it anyway because the screen is a phone.

Container queries answer the right question: "how wide is my container?" The same component works correctly everywhere it is placed, without knowing or caring about the screen.

Example

This grid starts at four columns and simplifies as the container narrows. Because these docs use .container on the content area, the grid responds to the content width, not your viewport:

1
2
3
4
<div class="container">
  <div class="grid grid-cols-4 md:grid-cols-2 sm:grid-cols-1 gap-md">
    <div>1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
  </div>
</div>

A Note on Naming

If you are coming from Tailwind, the prefixes will feel backwards. Tailwind's sm: means "from the small breakpoint upward". Axiom's sm: means "when the container is small". We think this reads more naturally, but it requires a mental shift.

Tailwind Axiom
sm:hidden Hide from 640px viewport and up Hide when container is small (≤ 600px)
md:grid-cols-2 Two columns from 768px viewport and up Two columns when container is medium (≤ 900px)
Direction Min-width (mobile-first, build up) Max-width (full-size default, simplify down)
Target Viewport (screen) Nearest container ancestor

Nested Containers

Container queries resolve to the nearest ancestor with container-type. This means containers nest naturally, and each level responds to its own context.

A common pattern is an outer container on your app shell, with inner containers on major layout regions:

<!-- Outer container: effectively viewport-width -->
<div class="container">

  <!-- Hamburger: md: fires based on the outer container -->
  <button class="hidden md:block">Menu</button>

  <aside class="sidebar">...</aside>

  <!-- Inner container: viewport minus sidebar -->
  <main class="container">

    <!-- Grid: md: fires based on main's width, not viewport -->
    <div class="grid grid-cols-3 md:grid-cols-1">...</div>

  </main>
</div>

In this layout, the hamburger's md: triggers based on the outer container (approximately the viewport). But the grid's md: triggers based on the <main> container, which is narrower because the sidebar takes up space. The same prefix does the right thing at each level because it is always asking: "how wide is my container?"

This is the key advantage over viewport queries. The grid does not need to know whether it is full-width or sharing space with a sidebar. It simply responds to the room it has.

Where to Place Containers

Put .container on layout-level elements:

Good Avoid
App shell / page wrapper Individual cards
Main content area List items
Sidebar panels Small UI components
Modal/dialog bodies Buttons or badges

If you put .container on a 300px-wide card, then md: and sm: are always active inside it, because the card is permanently below both thresholds. This is not useful. Let the card inherit its responsive behaviour from the layout container above it.

The exception is when you genuinely want a component to be independently responsive. A widget that might appear full-width on one page and in a sidebar on another is a reasonable candidate for its own .container. Just understand that the breakpoint values (900px, 600px) will be measured against that widget's width.

Viewport-Level Behaviour

Because Axiom has no viewport media queries, you handle "screen-wide" decisions through the outer container pattern. A .container on your app shell or <body> wrapper is effectively viewport-width, so md: and sm: on its direct children behave like traditional viewport breakpoints.

<body>
  <div class="container">

    <!-- These respond to the outer container (~viewport width) -->
    <nav class="flex md:hidden">Desktop nav</nav>
    <button class="hidden md:block">Mobile menu</button>

    <!-- This responds to its own container (the main area) -->
    <main class="container">
      ...
    </main>

  </div>
</body>

The outer .container gives you viewport-like control without a single @media rule. Navigation, app chrome, and layout scaffolding sit at this level. Component grids and content sit inside their own containers.

Breakpoint Values

The thresholds are 900px and 600px. These values are hardcoded in the CSS because @container conditions cannot reference CSS custom properties. This is a CSS specification limitation, not a design choice. If container queries gain variable support in the future, these will become configurable tokens.

md: fires at 900px container width and below
sm: fires at 600px container width and below
No lg: breakpoint exists. Default (unprefixed) styles cover everything above 900px.