Flexbox Alignment with justify-content and align-items Across Both Axes
Explains flexbox alignment properties justify-content and align-items with complete code samples, naming the 2014 hacks each value replaces.
A row of cards will not sit where you want them, and the fix is two declarations, not a utility class per item. The flexbox alignment system, in the fewest declarations that actually work, is justify-content for the main axis and align-items for the cross axis, set on the flex parent. This is what every layout question stops on: how these two properties distribute space, and the minimal pair of declarations for each common outcome. The old tricks this replaces are the justify-content: space-between with a ghost last-item hack, the margin-auto push-to-edge routine, and the inline-block-with-vertical-align-middle centering rig.
The Two Axes and Which Property Owns Each
Flexbox is one-dimensional. That single dimension is the entire reason justify-content and align-items exist as separate properties. The main axis is the direction of flex-direction: row runs left to right, and column runs top to bottom. justify-content distributes space along that main axis. The cross axis is the other direction, perpendicular to it, and align-items controls how each item sits across it.
The failure that trips everyone is axis confusion. In a row, justify-content: center centres horizontally, and align-items: center centres vertically. Flip the parent to flex-direction: column, and both swap: justify-content now centres vertically, align-items horizontally. Name the axis explicitly.
Two-Declaration Centring
Here is the minimal pattern for the most common outcome, centring on both axes:
.container {
display: flex;
justify-content: center;
align-items: center;
}
Two declarations. No margins, no vertical-align, no display: table-cell. It replaces the explicit-height-plus-margin: auto pattern and the table-cell centring hack outright, provided the parent has a real dimension on the cross axis. If the parent has no height in a row layout, align-items: stretch has nothing to stretch against, and items collapse to their content height.
justify-content Values, With Examples
justify-content accepts a family of distribution keywords. Each changes where the free space goes after items are sized. The initial value is flex-start, which packs items toward the start of the main axis with no extra space between them. flex-end packs them toward the end. center packs them in the middle, leaving equal free space on both sides.
The three space-distribution values are where the detail lives.
Which Space Value Does What
space-between places the first item flush with the start edge and the last item flush with the end edge, then inserts equal space between every adjacent pair. No outer gaps. space-around gives each item half-size gaps on both sides, so the outer gaps are half the inner ones. space-evenly gives every gap, including the outer two, the same size.
A concrete example clarifies the difference, and it is the one place a ghost item used to be required:
.nav {
display: flex;
justify-content: space-between;
}
.nav--evenly {
justify-content: space-evenly;
}
The old space-between with a ghost last-item trick, where you inserted an invisible empty element to force the real items apart, is dead. space-between does it with one declaration, and space-evenly gives you the visual result that trick never cleanly achieved. For a nav bar with three links where the first and last must touch the parent’s edges, space-between is the answer. For a toolbar where every gap should be identical, space-evenly wins.
align-items, Stretch, and Baseline
align-items has its own family. Its initial value is stretch, which is why flex items fill the cross axis by default. In a row layout, every item stretches to the parent’s height unless the parent has no explicit height, in which case the tallest item defines the cross size and the rest stretch to match it. The other values are flex-start, flex-end, center, plus two baseline positions.
Baseline Alignment
Baseline alignment does something no margin hack ever did. baseline and first baseline line up the text baselines of items regardless of their padding, font size, or internal structure. last baseline does the same for the last line of each item. This matters for a row of buttons with different font sizes or a set of form labels where the text must sit on a shared line.
The stretch behaviour is also the reason align-items needs a defined cross-axis dimension on the parent. If you want vertical centring in a row and the parent has no height, stretch has nothing to fill, and you get a collapsed row. Give the parent a height, and the two-declaration centring pattern from the first section works as written.
Here is the stretch-versus-center contrast:
.album {
display: flex;
align-items: stretch;
height: 300px;
}
.album--centered {
align-items: center;
}
The first rule makes every child fill the 300-pixel height. The second keeps the height but shrinks each child to its content height, centred vertically. One declaration separates a card grid that looks intentional from one that looks broken.
align-items Versus align-content When flex-wrap Is Active
The single most common mistake after axis confusion is expecting align-items to distribute rows of wrapped items. It does not. align-items aligns each individual item along the cross axis within its own line. align-content distributes the lines themselves along the cross axis when there is more than one line, which only happens when flex-wrap: wrap is set.
With flex-wrap: nowrap, there is only ever one line, so align-content has nothing to distribute. The moment you add flex-wrap: wrap, items can form multiple lines, and align-content takes over the space distribution among those lines. align-items still works per item, but the space between lines is align-content’s job.
A runnable sample shows the interaction:
.grid {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-content: space-between;
height: 400px;
}
.grid-item {
width: 30%;
height: 80px;
}
The items sit at the start of their own line because of align-items: flex-start. The lines themselves spread out with space between them because of align-content: space-between. Remove align-content, and the lines pack together at the start with no vertical distribution. Remove align-items, and each item stretches to its line’s height, changing the whole look.
Baseline Support
This is also where the baseline support question surfaces. Both align-content and align-items have Baseline status, widely available across Chrome, Firefox, Safari, and Edge since September 2015. Safari 8 and earlier required the -webkit- prefix. Internet Explorer 10 used -ms-flex-pack and -ms-flex-align. IE11 shipped full unprefixed support. The @supports guard below is the safe fallback for any environment that predates those engines.
Why Flex Items Do Not Wrap and the Overflow Failure
Flex items do not wrap because flex-wrap: nowrap is the initial value. The spec chose single-line layout so that a bare display: flex immediately gives you a row. The cost is that items shrink to fit, and if they cannot shrink enough, they overflow the parent.
When you set flex-wrap: nowrap explicitly, you tell the browser to keep all items on one line no matter what. If the combined minimum sizes exceed the parent width, the overflow goes in the direction of the main axis. In a left-to-right row, content spills off the right edge, invisible and unreachable without horizontal scrolling the page may not provide.
Silent Failure and the Fix
The failure mode is silent. No error, no warning: content is clipped or overflowing. The fix is one declaration: flex-wrap: wrap. It lets items form new lines when they run out of space. The tradeoff is that align-content becomes relevant, and you have to decide how the lines distribute.
A complete failure-and-fix pair:
.overflowing {
display: flex;
flex-wrap: nowrap;
}
.overflowing > * {
flex: 0 0 300px;
}
.wrapping {
display: flex;
flex-wrap: wrap;
}
With .overflowing, three 300-pixel items in a 700-pixel parent overflow by 200 pixels. With .wrapping, the third item moves to a second line. If you need the items to stay on one line, do not fight the default. Give them flexible sizing, flex: 1 1 auto or a min-width that permits shrinking.
The gap Property, Space Distribution, and What It Costs
The gap property adds fixed spacing between flex items before justify-content distributes the remaining free space. This order matters: gap reserves its space first, then justify-content works with whatever is left. The shorthand sets both row-gap and column-gap. In a row layout, row-gap controls the cross-axis spacing between wrapped lines, and column-gap controls the main-axis spacing between items.
No Margins, No Negative Counter
Flexbox gap shipped later than grid gap, which is why older tutorials use margins on items with a negative margin on the parent. That margin hack had a data-loss failure mode: the negative margin could pull the parent wider than its container, and the last item’s margin could collapse in ways that broke the layout. gap does not collapse and needs no negative counter.
The performance cost of gap is the same as any layout-affecting property on the parent: it triggers layout on the parent and all its children. It does not trigger paint or composite by itself, but it changes the geometry, so the browser recomputes positions. One gap declaration is cheaper than the equivalent margin system because it is one property instead of two per child, and it cannot be misapplied to the wrong side.
For a wrapped grid, the pattern is three declarations on the parent:
.photo-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
That replaces the entire margin-plus-negative-margin dance.
Flexbox Alignment Browser Support: What Shipped, What Is Safe
Flexbox alignment is old enough that the support story is mostly settled, but gaps are real for a small slice of users. Chrome shipped justify-content and align-items in version 29. Firefox shipped both in version 20. Safari shipped version 9. Edge shipped version 12. The perfect-support figure on a caniuse summary hides the fraction of a percent of users on device-locked browsers: iOS Safari on unsupported devices and Android WebView inside apps that never update. Check caniuse for the latest numbers.
The @supports Guard
The fallback is a single @supports guard. Write the old approach outside the guard and the flexbox approach inside it. For justify-content and align-items, the old techniques were margin: auto on flex items, text-align on the parent for inline content, and manual percentage-based spacing for distribution. None survive contact with a modern flex layout, so the guard has to exclude them entirely.
A complete fallback block:
.fallback-row {
display: block;
text-align: center;
}
.fallback-row > * {
display: inline-block;
vertical-align: middle;
}
@supports (display: flex) {
.fallback-row {
display: flex;
justify-content: center;
align-items: center;
text-align: initial;
}
.fallback-row > * {
display: block;
vertical-align: initial;
}
}
The guard covers both properties because they ship in the same engine versions. If display: flex is supported, justify-content and align-items are supported everywhere that matters. The fallback is for the device-locked remnants, not for modern browsers.
Safe and Unsafe Alignment, place-content, and Shortcuts
The safe and unsafe keywords modify how alignment behaves when free space is negative, meaning the content is larger than the parent. safe prevents data loss by falling back to start alignment when the requested alignment would push content out of view. unsafe allows the overflow, accepting that content may be clipped or inaccessible.
The default is unsafe. That is why justify-content: center on a parent with overflowing items can clip the start of the content. Adding safe gives you the fallback free:
.toolbar {
display: flex;
justify-content: safe center;
}
One declaration centres when there is room and falls back to left alignment when there is not. The same applies to align-items: safe center for the cross axis.
Shorthand Properties
The shorthand place-content sets align-content and justify-content in that order. place-items sets align-items and justify-items. For flexbox, justify-items has no effect, so place-items on a flex parent only sets align-items. The fewest-declaration centring pattern is then one line:
.centered {
display: flex;
place-items: center;
}
That sets align-items: center and leaves justify-content at its initial flex-start. To centre on both axes, place-content: center also works, but it requires the parent to have multiple lines or a defined cross size for align-content to act.
The real saving is not character count. place-content and place-items let you write one property instead of two, and the order is fixed, so there is no ambiguity about which axis gets which value. Baseline status matches the individual properties because the shorthands landed in the same engines.
align-self, Per-Item Overrides, and When Not to Use Them
align-self overrides align-items for a single flex item. It accepts the same values: stretch, center, baseline, flex-end, and the safe and unsafe modifiers. It is the escape hatch for the one card in a row that should sit at the bottom while its siblings are centred.
Two Declarations for the Exception
The pattern is two declarations total: one on the parent for the default, one on the item for the exception.
.card-row {
display: flex;
align-items: center;
height: 300px;
}
.card-row .featured {
align-self: flex-end;
}
The parent centres every card vertically. The featured card drops to the bottom edge. No extra wrapper, no margin computation, no position: absolute.
When Overuse Becomes the Problem
The failure mode with align-self is overuse. If you find yourself putting align-self on half the items, the parent’s align-items is wrong, not the individual items. The declaration belongs on the parent by default. align-self exists for the genuine exception, not as a substitute for thinking about the cross-axis distribution.
align-self also participates in the fallback story. In Internet Explorer 10, the prefixed property was -ms-flex-item-align, and IE11 supported the unprefixed name. The @supports guard for display: flex covers it, provided you put the align-self declarations inside the guard alongside align-items.
The Geometry Error: Expecting align-items to Work Without a Cross-Axis Dimension
The most common align-items failure is not a syntax error. align-items: center on a row parent with no height has nothing to centre against. The parent’s height is determined by its content, so every item is already at the same height as the tallest sibling. Centring is a no-op.
Give the parent a height, or a min-height. Once it has a dimension on the cross axis, align-items has free space to distribute. The same logic applies to align-content: with flex-wrap: wrap and no parent height, there is no space between lines to distribute.
A runnable example of the failure and the fix:
.no-height {
display: flex;
align-items: center;
}
.with-height {
display: flex;
align-items: center;
height: 300px;
}
The first rule does nothing visible unless the items themselves have different heights. The second rule centres every item vertically within the 300-pixel space. If you are centring vertically and nothing moves, check the parent’s height before you check the property value.
FAQ
Why does justify-content not centre my flex item vertically?
Because justify-content works on the main axis, which is horizontal in a row. Vertical centring is align-items’ job on that same row. Switch the parent to flex-direction: column, and justify-content centres vertically while align-items handles horizontal.
Can I use gap with justify-content: space-between?
gap reserves space between items first, then justify-content distributes the remaining free space. With space-between, the gap becomes the minimum distance between items, and any leftover space is added on top of it. The outer edges stay flush because space-between does not add outer gaps.
Do I need a fallback for justify-content now?
Only for device-locked browsers on older iOS Safari and Android WebView. Chrome, Firefox, Safari, and Edge all shipped unprefixed support by September 2015. The @supports (display: flex) guard is the standard fallback and costs one extra block.
What is the difference between space-between and space-evenly?
space-between puts the first item flush with the start edge and the last item flush with the end edge, with equal space between adjacent items. space-evenly gives every gap, including the two outer ones, the same size. space-around is the middle ground with half-size outer gaps.
Who This Subject Suits, and Who Should Walk Away
Flexbox alignment suits the working front-end developer who writes CSS daily and needs to know what shipped, what is safe to use, and what the fallback is without reading five blog posts. It suits the design-system author who needs precise specification behaviour, interop quirks, and the vocabulary to defend choices to stakeholders. It suits the performance-conscious developer who understands that gap costs layout, that animating anything other than transform and opacity triggers layout or paint, and that the compositor advantage is lost the moment you animate a property that changes geometry.
It does not suit someone learning to code from zero. Send them to web.dev/learn/css or the MDN CSS first-steps guide, and they can return later. It does not suit someone debugging a React state bug, because that is a JavaScript question, and flexbox will not fix it. It does not suit someone comparing CSS-in-JS libraries, because that is a JavaScript tooling question, and the alignment properties behave identically regardless of how the CSS is delivered.
The space-between with a ghost last-item trick, the margin-auto push-to-edge routine, and the inline-block-with-vertical-align-middle centring rig all die to the same two declarations. The only reason to keep them alive is a device-locked browser that still needs the @supports guard.