uxskill
Star on GitHub

Layout · 2026-06-04

Responsive by construction: layouts with zero media queries.

Ask any AI coding tool for a card grid and you get a breakpoint table: one column on mobile, two at 768px, three at 1024px. It works until the component moves into a narrower slot, and then the breakpoints lie. There is a better default, and it needs no media query at all.

Why the breakpoint reflex is wrong

A media query asks the wrong question. It measures the viewport. But a card grid does not live in the viewport; it lives in a column, a modal, a sidebar, a dashboard cell. The same grid can sit in a 1200px hero one screen and a 320px drawer the next. A breakpoint tuned to the viewport has no idea which of those it is in, so it guesses, and the guess is wrong every time the component is reused somewhere it was not designed for.

AI tools reach for breakpoints anyway, because breakpoints are what the training data is full of. A decade of Bootstrap and Tailwind tutorials taught the model that "responsive" means a md: prefix. So the model ships grid-cols-1 md:grid-cols-2 lg:grid-cols-3 on reflex, the layout looks fine in the preview, and it breaks the moment a human nests it.

Two primitives that ask the right question

The fix is to let the content decide. Two CSS features do this without a single size threshold.

1. The auto-fit grid

Tell the grid a minimum comfortable column width and let it pack in as many as fit. No column count, no breakpoint.

.grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns:
    repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}

One column in a 320px drawer, four in a 1200px hero, and every count in between, all from one rule. The min(16rem, 100%) is the part most hand-written grids miss: without it, a 16rem track is wider than a 320px parent and the layout overflows with a horizontal scrollbar. With it, the track caps at the parent width and the overflow is impossible.

2. The container query

When a component needs to change its internal shape, not just its column count, it should respond to its own box, not the page. Mark a parent as a query container and the child reacts to the space it actually has.

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

.card { display: grid; gap: 1rem; }

@container (min-width: 28rem) {
  .card { grid-template-columns: 8rem 1fr; }
}

Drop that card into a wide region and the image sits beside the text. Drop the identical card into a narrow rail and it stacks. The component is now portable: it carries its own responsiveness instead of depending on a viewport table maintained somewhere else. Note that @container is not a size-based @media query. It is the one query type that survives reuse.

What the engine emits

This is the rule ux-skill follows when it composes layout. Its seven layout primitives (stack, cluster, grid, sidebar, cover, frame, and split) are built on auto-fit minmax and intrinsic, content-driven sizing, with container queries reserved for the components that need to change their internal shape. The generated layout CSS contains no size-based @media query at all. The only media queries that survive are the ones tied to user intent rather than screen width, chiefly prefers-reduced-motion, which is about a person's settings, not a breakpoint.

That constraint is testable, which is the point. The linter treats a viewport breakpoint on a component-level grid as a tell, the same way it treats Inter at 90px or a three-equal-card row. You can read every rule it checks at the anti-patterns catalogue, and the layout commands that produce this CSS are listed under commands.

When you still want a media query

Container queries replace component breakpoints, not page architecture. A media query is still the honest tool for whole-page decisions: collapsing a top navigation into a drawer, switching a two-pane app shell to a single stack, or changing the page grid template. The rule is simple. If the decision is about the document, use a media query. If it is about a component that could appear anywhere, use a container query or an auto-fit grid and let the box decide.

A breakpoint encodes an assumption about where a component will live. Container queries let it work without one.

Try it

pip install uxskill
uxskill recommend --brief-file=.ux/last-discovery.json
uxskill lint ./src --threshold high

The recommender returns layout primitives wired this way by default, and the linter flags component-level breakpoints on the way back. The source is on GitHub if you want to read the exact CSS the layout module produces.

Related