Writing

Modern CSS Techniques for Better Layouts

Four CSS features worth using today: container queries, :has(), clamp(), and subgrid, with practical examples and fallbacks.

Modern CSS lets components respond to their own space.

CSS has changed a lot in the last few years. Features that used to require JavaScript or a complex build step are now ordinary CSS. The useful question is not whether they are new, but whether they make the interface simpler.

These are four techniques I reach for regularly.

Container queries: components that respond to their container

Media queries ask how wide the viewport is. Container queries ask how wide the component’s parent is. That is a better fit for reusable cards.

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

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

@container (min-width: 32rem) {
  .card {
    grid-template-columns: 12rem 1fr;
    align-items: start;
  }
}

The card now adapts whether it sits in a sidebar or a full-width page. No JavaScript, no extra class names, no guessing from the viewport.

A few practical details:

  • container-type: inline-size enables size queries on the inline axis.
  • The container must be an ancestor of the elements being queried.
  • Give the container a name if nesting makes the target ambiguous: container: card-grid / inline-size; and @container card-grid (min-width: 32rem).

Container queries are supported in current versions of all major browsers. If you need older support, keep the single-column layout as the default and let the query be an enhancement.

:has(): style a parent based on its children

:has() lets a selector look at descendants. It is useful when the state of a child should change the parent’s layout.

.form-field:has(input:invalid) {
  border-color: #dc2626;
}

.form-field:has(input:focus-visible) {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

.card:has(img) {
  padding-top: 0;
}

This reduces the need for state classes like .is-invalid or a wrapper element whose only job is to be a styling hook.

Use it carefully. :has() is powerful, but a selector that reaches too far can become hard to debug. Prefer it for local relationships, not page-wide conditional styling.

clamp(): fluid type without breakpoint soup

clamp() takes a minimum, a preferred value, and a maximum:

.page-title {
  font-size: clamp(2rem, 5vw + 1rem, 4.5rem);
  line-height: 1.05;
  letter-spacing: -0.04em;
}

The title grows with the viewport but never becomes unreadably small or comically large. The middle value can combine viewport units and a fixed value, which is often more predictable than pure vw.

Use fluid type for display text, not body copy. Body text still benefits from readable constraints:

.prose {
  max-width: 68ch;
  line-height: 1.75;
}

ch is based on the width of the zero character, so it tracks the font instead of an arbitrary pixel value.

Subgrid: align across nested grids

Grid is already good at two-dimensional layout. Subgrid fixes the case where a nested grid needs to align with its parent.

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}

Now the title, body, and action rows line up across cards even when the content lengths differ. This is much simpler than fixed heights or JavaScript measurement.

Subgrid is supported in current browsers but has a shorter support history than the other features here. For older browsers, the card still renders as a normal grid. That is a good fallback because the layout remains usable.

One more: respect motion preferences

Modern CSS is not only about layout. If you animate anything, include the reduced-motion case:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}

This is not a nice-to-have. For some people, large motion can cause dizziness or nausea. The fallback should be part of the component, not an afterthought.

How to decide what to use

Start with the layout problem. If a component needs to adapt to its own space, reach for container queries. If a child’s state should affect the parent, try :has(). If text needs to scale smoothly, use clamp(). If nested rows need to align, use subgrid.

Then check two things: can the layout degrade gracefully, and does the CSS still make sense to the next person reading it?

New features are only useful when they remove complexity, not when they add a clever trick.