Flexbox Patterns
flex-wrap, gap, shrink, and grow for layouts that reflow naturally across screen sizes. You'll hit this when items squish into a single row on small screens instead of wrapping naturally.
.card-row {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.card-row {
flex-direction: column;
}
}.card-row {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.card-row {
flex-direction: column;
}
}.card-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card-row > * {
flex: 1 1 300px;
}.card-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card-row > * {
flex: 1 1 300px;
}A max-width: 768px breakpoint is arbitrary. What if the container is in a sidebar and only 400px wide? The cards would still try to sit side by side because the *viewport* is wider than 768px. flex-wrap responds to actual available space.
flex-wrap: wrap with flex: 1 1 300px means each card wants to be at least 300px wide. When the container can't fit two 300px cards side by side, they automatically wrap without any breakpoint. The items also grow to fill available space.
.nav-links {
display: flex;
}
.nav-links > * {
margin-right: 1rem;
}
.nav-links > *:last-child {
margin-right: 0;
}.nav-links {
display: flex;
}
.nav-links > * {
margin-right: 1rem;
}
.nav-links > *:last-child {
margin-right: 0;
}.nav-links {
display: flex;
gap: 1rem;
}.nav-links {
display: flex;
gap: 1rem;
}Margin-based spacing requires removing the margin from the last child. If items wrap, you also need to handle the last item of *each row*, which :last-child doesn't cover. gap handles all of this automatically.
gap applies spacing *between* flex children only, so there's no extra margin on the first or last item and no :last-child override needed. It also works correctly when items wrap: no trailing gap on the last item of each row.
.toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-actions {
display: flex;
gap: 0.5rem;
}.toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-actions {
display: flex;
gap: 0.5rem;
}.toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-title {
flex: 1 1 0%;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-actions {
flex-shrink: 0;
display: flex;
gap: 0.5rem;
}.toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toolbar-title {
flex: 1 1 0%;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-actions {
flex-shrink: 0;
display: flex;
gap: 0.5rem;
}Without min-width: 0, the title's min-width: auto prevents it from shrinking below its content width, so the ellipsis never activates and the toolbar overflows. Without flex-shrink: 0, the action buttons may also compress.
flex: 1 1 0% makes the title take remaining space and shrink when needed. min-width: 0 overrides the default min-width: auto so text truncation actually works. flex-shrink: 0 on actions prevents buttons from compressing.
.layout {
display: flex;
}
.sidebar { width: 250px; }
.main { width: calc(100% - 500px); }
.aside { width: 250px; }
@media (max-width: 768px) {
.layout { flex-direction: column; }
.sidebar, .main, .aside {
width: 100%;
}
}.layout {
display: flex;
}
.sidebar { width: 250px; }
.main { width: calc(100% - 500px); }
.aside { width: 250px; }
@media (max-width: 768px) {
.layout { flex-direction: column; }
.sidebar, .main, .aside {
width: 100%;
}
}.layout {
display: flex;
flex-wrap: wrap;
}
.sidebar { flex: 0 0 250px; }
.main { flex: 1 1 600px; }
.aside { flex: 0 0 250px; }.layout {
display: flex;
flex-wrap: wrap;
}
.sidebar { flex: 0 0 250px; }
.main { flex: 1 1 600px; }
.aside { flex: 0 0 250px; }Hardcoded calc(100% - 500px) breaks if either sidebar changes width. The 768px breakpoint is arbitrary and doesn't account for the actual content needs. flex-wrap with appropriate flex-basis values creates a self-adjusting layout.
With flex-wrap: wrap, the main content has a flex-basis of 600px. When the container is narrower than 250 + 600 + 250 = 1100px, items naturally wrap. No media query, no calc(), and it adapts to any container width.
function Header() {
const isMobile = useMediaQuery("(max-width: 640px)");
return (
<Stack
direction="row"
justifyContent={
isMobile ? "center" : "space-between"
}
>
<Logo />
<Nav />
<Actions />
</Stack>
);
}function Header() {
const isMobile = useMediaQuery("(max-width: 640px)");
return (
<Stack
direction="row"
justifyContent={
isMobile ? "center" : "space-between"
}
>
<Logo />
<Nav />
<Actions />
</Stack>
);
}function Header() {
return (
<Stack direction="row" alignItems="center">
<Logo />
<Box sx={{ flex: 1 }} />
<Nav sx={{ display: { xs: "none", sm: "flex" } }} />
<Actions sx={{ ml: { xs: "auto", sm: 0 } }} />
</Stack>
);
}function Header() {
return (
<Stack direction="row" alignItems="center">
<Logo />
<Box sx={{ flex: 1 }} />
<Nav sx={{ display: { xs: "none", sm: "flex" } }} />
<Actions sx={{ ml: { xs: "auto", sm: 0 } }} />
</Stack>
);
}useMediaQuery causes a hydration mismatch: the server renders space-between (desktop), then React corrects to center on mobile after hydration. The spacer + display approach avoids this entirely. ml: "auto" pushes the actions to the right when nav is hidden.
A flex spacer (flex: 1) pushes nav and actions to the right. On mobile, nav is hidden with CSS and ml: "auto" pushes the actions to the far right. No useMediaQuery, no hydration flash, and the layout is controlled entirely with CSS.