Flexbox has been production-ready since around 2015, yet it remains a frequent source of confusion — not because the spec is poorly designed, but because most introductions lead with property lists rather than the underlying model. Once you internalize the axis system that governs every flexbox decision, the properties stop feeling arbitrary and start reading like a coherent vocabulary. That model sits on top of the same CSS box model that governs every other element — flex items still have margin, border, padding, and content, and box-sizing still determines what a flex item’s declared width actually measures.

The Axis Model: The One Mental Model You Need

Every flex container operates on two perpendicular axes. The main axis runs in the direction flex items are placed. The cross axis runs perpendicular to it. That distinction — which axis is which — controls the behavior of nearly every flexbox property.

By default, flex-direction: row makes the main axis horizontal (left to right in left-to-right writing modes) and the cross axis vertical. Flip to flex-direction: column and those axes rotate: the main axis becomes vertical, the cross axis horizontal.

This matters enormously because justify-content always operates on the main axis, while align-items always operates on the cross axis. Developers who memorize “justify is horizontal, align is vertical” immediately run into trouble when they switch to a column layout. The correct mental model: justify follows the main axis, align follows the cross axis — and the axes are determined by flex-direction.

.container {
  display: flex;
  flex-direction: row; /* main axis: horizontal */
  justify-content: space-between; /* distributes along horizontal axis */
  align-items: center; /* aligns along vertical axis */
}

Switch to column:

.container {
  display: flex;
  flex-direction: column; /* main axis: vertical */
  justify-content: space-between; /* now distributes vertically */
  align-items: center; /* now aligns horizontally */
}

The same properties, rotated behavior. Once this clicks, you stop second-guessing which property to reach for.

flex-direction and Writing Mode Awareness

The CSS Flexbox Level 1 specification defines flex-direction as setting the direction of the main axis within the flex container. The four values — row, row-reverse, column, column-reverse — control both direction and orientation.

row and row-reverse produce horizontal main axes; items flow left-to-right or right-to-left respectively. column and column-reverse produce vertical main axes; items flow top-to-bottom or bottom-to-top.

A subtlety worth knowing: row follows the writing mode of the document. In a right-to-left context (dir="rtl" or writing-mode adjustments), row flows right-to-left. If you need a directionally absolute layout regardless of writing mode, you have to account for this explicitly. For most Western web projects this does not surface, but it becomes relevant in internationalized UIs. The MDN documentation on flex-direction covers the writing-mode interaction thoroughly.

Distributing Space: justify-content

justify-content controls how leftover space on the main axis is distributed among flex items — or, more precisely, how items are positioned within the container when there is free space (or overflow).

The most-used values:

  • flex-start — items pack toward the start of the main axis
  • flex-end — items pack toward the end
  • center — items cluster at the midpoint
  • space-between — first item at start edge, last item at end edge, remaining space distributed evenly between items
  • space-around — equal space on both sides of each item (so items at edges have half the space of items in the middle)
  • space-evenly — equal space between every item and between items and the container edges

For a navigation bar, space-between tends to produce the most natural result: brand on the left, nav links on the right, with the gap determined by the container width.

.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 1.5rem;
}

Aligning Items: align-items and align-self

align-items controls item positioning on the cross axis for all items in the container. Its default value is stretch, which causes flex items to fill the cross-axis dimension of the container — this is why flex children in a row container become the same height as the tallest sibling without any additional CSS.

Key values:

  • stretch — items expand to fill the cross-axis size of the container (default)
  • flex-start — items align to the start of the cross axis
  • flex-end — items align to the end
  • center — items center on the cross axis
  • baseline — items align so their text baselines are co-planar

align-self is the per-item override. Apply it to individual flex children to break them out of the container’s align-items rule.

.card-row {
  display: flex;
  align-items: flex-start; /* cards align to top */
}

.card-row .featured {
  align-self: stretch; /* featured card fills full height */
}

The flex Property: Grow, Shrink, and Basis

This is where most flexbox confusion concentrates. The flex shorthand packs three sub-properties: flex-grow, flex-shrink, and flex-basis. Understanding each separately — and then together — makes the shorthand readable.

flex-basis sets the starting size of a flex item along the main axis before any space distribution occurs. It behaves like width in a row container and like height in a column container. The value auto (the default in the longhand, not the shorthand) defers to the item’s width or height property if set, or its content size if not.

flex-grow sets the growth factor. When there is leftover space in the container after items are sized to their basis, that space is distributed proportionally among items with a non-zero flex-grow. An item with flex-grow: 2 absorbs twice as much free space as an item with flex-grow: 1.

flex-shrink sets the shrink factor. When items collectively exceed the container size, they shrink proportionally. An item with flex-shrink: 0 refuses to shrink — useful for fixed-size elements like icons or avatars.

The algorithm, in brief:

  1. Size each item to its flex-basis
  2. Calculate free space: container size minus sum of all flex-basis values
  3. If free space is positive, distribute it among items weighted by flex-grow
  4. If free space is negative (overflow), reduce items weighted by flex-shrink

The flex shorthand has some non-obvious defaults. flex: 1 expands to flex: 1 1 0% — note the basis becomes 0%, not auto. This means items start from zero width and grow proportionally from there, which produces equal-width siblings regardless of content. flex: auto expands to flex: 1 1 auto, where content size influences the distribution. The distinction matters when item content sizes are unequal and you want equal sizing versus proportional-to-content sizing.

/* Equal-width columns regardless of content */
.column {
  flex: 1;
}

/* Proportional to content, but still flexible */
.column {
  flex: auto;
}

/* Fixed size, no growth or shrink */
.sidebar {
  flex: 0 0 280px;
}

For a sidebar-plus-content layout, a common and robust pattern:

.layout {
  display: flex;
}

.sidebar {
  flex: 0 0 280px; /* fixed width, no growth, no shrink */
}

.main-content {
  flex: 1; /* takes all remaining space */
  min-width: 0; /* prevents overflow from long content */
}

The min-width: 0 on the content area is a frequently overlooked fix. Flex items default to min-width: auto, which means they will not shrink below their content size. Setting min-width: 0 allows the browser to shrink the item as needed and prevents text overflow issues in layouts with constrained widths. This pattern is discussed in the CSS Working Group’s notes on minimum sizing and is worth keeping in your toolkit.

Wrapping: flex-wrap and align-content

By default, flex items do not wrap — they stay on a single line and either overflow or shrink. Setting flex-wrap: wrap allows items to flow onto additional lines when they cannot fit within the container.

When wrapping occurs, the container now has multiple flex lines, and a third alignment property enters the picture: align-content. Where align-items positions items within a single line on the cross axis, align-content positions the lines themselves within the container on the cross axis.

If there is only one line (no wrapping, or all items fit on one line), align-content has no effect.

.card-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  align-content: flex-start; /* lines pack toward top */
}

.card {
  flex: 0 0 calc(33.333% - 1rem);
}

The gap property (previously grid-gap, now standardized for flex containers in CSS Box Alignment Level 3) provides consistent gutters without margin hacks. Browser support for gap in flex contexts is now complete across all modern browsers.

Common Patterns

Centering

Perfectly centering an element — horizontally and vertically — within a container is one line of CSS with flexbox:

.center-container {
  display: flex;
  justify-content: center;
  align-items: center;
}

This works because justify-content: center centers along the main axis and align-items: center centers along the cross axis. For a row container, that is horizontal then vertical. For a column container, it is vertical then horizontal — same result either way for centering.

.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.nav-links {
  display: flex;
  gap: 1.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

The outer nav distributes the logo and nav-links group to opposite ends. The inner nav-links flex container spaces the individual links. Nesting flex containers is entirely normal and encouraged.

Card Row with Equal Heights

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

.card {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.card-body {
  flex: 1; /* pushes footer to bottom */
}

Making the card itself a column flex container allows you to push card footers (CTAs, metadata) to the bottom edge regardless of content length. The card-body takes flex: 1 and absorbs all available space within the card, shoving whatever follows it to the bottom.

Responsive Sidebar Layout

.page-layout {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
}

.sidebar {
  flex: 0 0 280px;
  min-width: 0;
}

.main {
  flex: 1;
  min-width: 0;
}

@media (max-width: 768px) {
  .sidebar {
    flex: 0 0 100%;
  }
}

At narrow viewports, the sidebar expands to full width and wraps above the main content. At wider viewports, they sit side by side. This avoids media query breakpoints for many intermediate cases and makes the layout robust to content changes.

Order and Visual Rearrangement

The order property allows you to change the visual order of flex items without altering the DOM. Items default to order: 0; increasing the value moves an item later in visual order, decreasing it moves it earlier.

This is useful for reordering items responsively — placing a sidebar after the main content in DOM order (better for accessibility and SEO) while displaying it first visually on wide screens. However, be cautious: visual order that diverges from DOM order can create confusing tab and reading sequences for keyboard and screen reader users. The WCAG 2.1 Success Criterion 1.3.2 addresses meaningful sequence, and the CSS specification explicitly warns that authors should not use order to override a meaningful content order. Use it deliberately and verify keyboard navigation remains logical. This connects to the broader considerations covered in the accessibility and semantic structure article.

Gap vs. Margins

Before gap support was universal for flexbox, the common approach was adding margins to flex children and removing them from first or last children with :first-child/:last-child selectors, or using negative margins on the container. Both approaches are now unnecessary for most projects. gap sets gutter space between flex items only — it does not add space between items and the container edges, which is usually the desired behavior.

/* Old pattern — now avoidable */
.items > * + * {
  margin-left: 1rem;
}

/* Current pattern */
.items {
  display: flex;
  gap: 1rem;
}

For projects still supporting older browsers, caniuse.com data on flex gap shows coverage above 95% globally as of late 2024, with the remaining edge cases primarily in older Android WebView versions.

When to Reach for Grid Instead

Flexbox is a one-dimensional layout tool — it excels at distributing items along a single axis. When you need to control placement on both axes simultaneously — aligning items to specific rows and columns — CSS Grid is the appropriate choice. The distinction is not about complexity but about dimensionality. A nav bar is flexbox. A full page layout with header, sidebar, content, and footer in a defined grid is CSS Grid. Many components use both: a Grid layout at the page level with flex containers inside each grid area. The two layout systems are complementary, not competitive. For font and typography decisions that interact with these layout systems, the web fonts loading guide is a useful companion reference.

The CSS Flexbox specification remains the authoritative reference, and MDN’s flexbox documentation provides thorough coverage with browser compatibility tables that reflect the current state of support.