CSS Responsive Patterns with Flexbox

Flexbox is one of the easiest ways to build layouts that adapt to different screen sizes without writing a media query for every breakpoint. Because flex items can grow, shrink, and wrap automatically, you can describe a layout once and let the browser redistribute space as the viewport changes. This lesson covers the specific patterns that make Flexbox genuinely responsive: wrapping grids, direction-switching navigation, and the sizing math that decides when items grow, shrink, or break to a new line.

Overview / How it works

A responsive Flexbox layout relies on three ideas working together: the flex container, the sizing of flex items along the main axis, and wrapping behavior when items no longer fit on one line.

When you set display: flex on a container, its direct children become flex items laid out along a main axis (horizontal by default, controlled by flex-direction). Each item’s size along that axis is not just its width or height — it is computed from the flex-grow, flex-shrink, and flex-basis values, usually written together as the flex shorthand. The rendering engine first lays out every item at its flex-basis size, then compares the total to the container’s available space. If there is leftover space, it is distributed to items proportionally to their flex-grow value. If items overflow the container, the engine shrinks them proportionally to their flex-shrink value (weighted by size). This is why flex: 1 1 200px means “start at 200px, then grow or shrink to fill the row.”

Responsiveness comes from combining this with flex-wrap: wrap. Without wrapping, the browser is forced to keep every item on a single line and shrink them until they fit (or overflow if they hit their minimum size). With wrapping enabled, once items can no longer shrink to fit the current line at their specified flex-basis, the engine breaks to a new flex line, and remaining items flow onto it — much like text wrapping to a new line when it runs out of horizontal space. Each wrapped line then behaves like its own mini flex container for the purpose of growing/shrinking to fill available space.

This means a single ruleset like flex: 1 1 250px; flex-wrap: wrap; can produce a layout that shows 4 columns on a wide desktop, 2 on a tablet, and 1 on a phone — with no media queries at all, because the browser recalculates how many 250px-ish items fit on each line as the viewport resizes.

Syntax

.container {
  display: flex;
  flex-wrap: wrap | nowrap | wrap-reverse;
  flex-direction: row | row-reverse | column | column-reverse;
  gap: <length>;
}

.item {
  flex: <flex-grow> <flex-shrink> <flex-basis>;
}
  • flex-wrap — whether items are forced onto one line (nowrap, the default) or allowed to wrap onto multiple flex lines (wrap).
  • flex-direction — the main axis direction; switching this in a media query is how navigation bars turn into stacked menus.
  • flex-grow — a unitless number describing how much of the leftover space an item should absorb, relative to its siblings.
  • flex-shrink — a unitless number describing how much an item should shrink, relative to its siblings, when there isn’t enough room.
  • flex-basis — the starting main-axis size before growing/shrinking is applied; can be a length, percentage, auto (use the item’s width/content size), or a function like clamp().
  • gap — spacing between flex items and between wrapped lines, without needing margin hacks.

Examples

Example 1: A self-wrapping card grid

.card-container {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.card {
  flex: 1 1 250px;
  background: #f4f4f8;
  border-radius: 8px;
  padding: 1rem;
}

This applies to a <div class=”card-container”> containing several <div class=”card”> elements.

Result: On a wide screen, the cards sit in a single row, each growing beyond 250px to fill the available width evenly. As the viewport narrows, cards that no longer fit at roughly 250px wrap onto a new line — for example 4 across on desktop, 2 across on tablet, 1 per row on a narrow phone — with a consistent 1.5rem gap both between columns and between wrapped rows.

The 1 1 250px shorthand means: grow to fill space, shrink if needed, but start every card around 250px. Because flex-wrap: wrap is set, the browser recalculates how many 250px-ish cards fit per line at every viewport width — this is the core responsive Flexbox pattern.

Example 2: A navigation bar that stacks on small screens

.site-nav {
  display: flex;
  flex-direction: row;
  gap: 1rem;
  align-items: center;
}

@media (max-width: 600px) {
  .site-nav {
    flex-direction: column;
    align-items: stretch;
  }
}

Result: Above 600px viewport width, the navigation links are arranged horizontally in a row with 1rem of space between each, vertically centered. At 600px or narrower, the media query switches the main axis to vertical: links stack on top of each other, each stretched to the full width of the nav bar.

This pattern shows that Flexbox and media queries are complementary, not competing techniques. flex-wrap handles gradual reflow for many similar items (like cards), while explicitly switching flex-direction at a breakpoint is better for a small, fixed set of navigation items where you want a deliberate layout change rather than organic wrapping.

Example 3: Flexible columns using clamp() and a min-width fix

.gallery {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.gallery__item {
  flex: 1 1 clamp(150px, 30%, 300px);
  min-width: 0;
  overflow: hidden;
}

.gallery__item img {
  display: block;
  width: 100%;
  height: auto;
}

Result: Each gallery item’s target size scales with the container: never smaller than 150px, never larger than 300px, and otherwise about 30% of the container’s width. Combined with wrapping, this produces roughly 3 columns on wide screens and fewer as the viewport shrinks, without a single media query. The min-width: 0 override prevents images or long unbreakable content from forcing the item wider than intended (explained in Common Mistakes below).

clamp(150px, 30%, 300px) is evaluated once per layout pass as the flex-basis, so it responds fluidly to the container’s width rather than jumping between fixed breakpoints.

How it works step by step

  1. The browser computes each flex item’s hypothetical main size from its flex-basis (resolving percentages and functions like clamp() against the container’s content box).
  2. It sums these sizes plus any gap values along the main axis for the current flex line.
  3. If flex-wrap: wrap is set and that sum exceeds the container’s available width, the engine moves the item that doesn’t fit — along with everything after it — onto a new flex line, and repeats the process for that line.
  4. Within each line, if there is leftover space, it’s distributed to items according to their flex-grow ratio; if there’s a shortfall (and wrapping isn’t possible or a line still overflows), space is removed according to each item’s flex-shrink ratio, weighted by its base size.
  5. Finally, cross-axis alignment (align-items, align-content) positions items and lines perpendicular to the main axis, and gap inserts fixed spacing between both items and wrapped lines.

Common Mistakes

Mistake 1: Forgetting flex-wrap and expecting items to reflow

Wrong:

.row {
  display: flex;
}

.row .item {
  flex: 1 1 250px;
}

Without flex-wrap: wrap, the container defaults to nowrap. On a narrow screen, instead of wrapping to new lines, every item is squeezed down (via flex-shrink) to fit on one line, often becoming too narrow to read.

Corrected:

.row {
  display: flex;
  flex-wrap: wrap;
}

.row .item {
  flex: 1 1 250px;
}

Mistake 2: Ignoring the default min-width: auto on flex items

Wrong:

.row {
  display: flex;
  gap: 1rem;
}

.row .item {
  flex: 1 1 0;
}

.row .item img {
  width: 100%;
}

Flex items have an implied minimum main-size of auto, which resolves to the size of their content (like an image’s intrinsic width or an unbreakable long word). Even though flex-shrink is set, the browser refuses to shrink the item below that content size, so images or long strings can overflow their container or force siblings to be pushed out unexpectedly.

Corrected:

.row {
  display: flex;
  gap: 1rem;
}

.row .item {
  flex: 1 1 0;
  min-width: 0;
}

.row .item img {
  width: 100%;
}

Setting min-width: 0 explicitly removes that content-based floor, allowing the item (and the image inside it, since it’s set to width: 100%) to shrink properly with its container.

Best Practices

  • Use flex-wrap: wrap together with a reasonable flex-basis (like 1 1 250px) as your first tool for grids of similar cards — it often removes the need for several media query breakpoints entirely.
  • Reserve flex-direction switches inside media queries for structural changes, such as turning a horizontal nav into a stacked mobile menu.
  • Always add min-width: 0 (or min-height: 0 for column layouts) to flex items that contain images, long text, or code blocks, to avoid unexpected overflow.
  • Prefer gap over margin-based spacing hacks — it handles both row and column spacing cleanly and doesn’t require negative-margin tricks on the container.
  • Use clamp() in flex-basis for fluid column sizing that scales smoothly instead of jumping at fixed breakpoints.
  • Test with real content lengths (long titles, large images) — Flexbox’s wrapping and shrinking behavior depends heavily on actual content size, not just the numbers in your CSS.

Practice Exercises

  1. Build a <div> container of six <div> “product” items using flex-wrap: wrap and a flex-basis of your choice so that it shows 3 items per row on a wide screen and 1 per row on a narrow one, with a consistent gap. Try resizing the viewport instead of adding media queries.
  2. Take the navigation example from this lesson and modify the media query breakpoint to 800px, then add a third link. Predict, then verify, how many links fit per row before it wraps.
  3. Create a two-item flex row where one item contains a very long unbroken string (like a URL). First omit min-width: 0 and observe the overflow, then add it and describe how the layout changes.

Summary

  • flex-wrap: wrap lets items flow onto new lines automatically as space runs out, which is the foundation of responsive Flexbox layouts.
  • flex-basis sets each item’s starting main-axis size before flex-grow/flex-shrink redistribute leftover or missing space.
  • Combining a flex-basis length with wrapping produces layouts that adapt continuously to viewport width, often without media queries.
  • Media queries are still useful for deliberate structural changes, like switching flex-direction for navigation.
  • Flex items have a default min-width: auto that can prevent proper shrinking — override it with min-width: 0 when content might overflow.
  • gap is the modern, hack-free way to space both items and wrapped lines.