Complex conditional width using flex-basis with clamp
The primitive Sidebar layout is a kind of quantum layout whereby the sidebar only exists as a sidebar where there is available space. As the Sidebar document details, wrapping from a two-column to a one-column layout depends on the relationship between two things:
- A minimum percentage width for the non-sidebar element
- A preferred width for the sidebar element
The breakpoint—if you want to call it that—is where (1) equals (2). When (1) becomes less than (2), the sidebar and non-sidebar stack vertically. The sidebar is no longer a sidebar.
(2) is either a set flex-basis
value or defaults to the (maximum) width of the sidebar’s content (implicitly auto
). Importantly, if the set value was a percentage, like (1), the breakpoint would never be breached and wrapping would not occur. Why? Because the sidebar would maintain its %
-based share of the component in spaces of any size.
A flex-basis
value of, say, 20ch
differs because it’s a kind of quasi-fixed value. It will be exactly 20ch
unless 20ch
is either too small or too large.
.sidebar {
flex-basis: 20ch;
/* ↓ when not a sidebar, let it use up all the space */
flex-grow: 1;
}
But what if you wanted the sidebar to be 33.333% wide where possible? You can make flex-basis
33.333% then add a min-inline-size
:
.sidebar {
flex-basis: 33.333%;
min-inline-size: min(20ch, 100%);
/* ↓ when not a sidebar, let it use up all the space */
flex-grow: 1;
}
The important part is using the target min-inline-size
inside the min()
function with 100%
—a technique borrowed from the Grid layout. Since 100% is really the maximum value for the context, we are effectively toggling min-inline-size
on and off, conditionally. Which is the kind of thing people don’t think CSS can do.
This toggle ensures 20ch
is only applied where that value is less than 100%
. In other words, it stops stuff overflowing the viewport on small screens. Why don’t we use a max-inline-size
declaration of 100%
instead? Because, in the CSS standard, min-inline-size
is designed to win out over max-inline-size
, in all cases. Fortunately, the min()
function is now well supported.
Another approach is to remove the min-inline-size
declaration altogether and use clamp()
for flex-basis
:
.sidebar {
flex-basis: clamp(20ch, 33.333%, 100%);
}
The clamp()
function clamps a value between upper and lower bounds. In this case, we prefer 33.333% but the width the 33.333% proportion represents should never be allowed to get smaller than 20ch
or larger than 100%
. Here’s the complete code for this specific take on the Sidebar component:
.with-sidebar {
display: flex;
flex-wrap: wrap;
gap: 1em;
}
.sidebar {
flex-basis: clamp(20ch, 33.333%, 100%);
flex-grow: 1;
}
.with-sidebar > :last-child {
flex-basis: 0;
flex-grow: 999;
min-inline-size: 50%;
}
The component’s algorithm is now more complex and nuanced than set out in the Sidebar layout. It can be expressed something like this: “Where the non-sidebar element is wider than the sidebar element (more than 50% of the whole component’s width), display the two elements side-by-side, wherein the width of the sidebar element is the smaller of 20ch
and 33.333%
. Where the non-sidebar element is narrower than the sidebar element (less than 50% of the whole component’s width), stack the two elements vertically, wherein the sidebar’s width will now be 100%
whether 100%
is wider or narrower than 20ch
.”
That’s perhaps an unexpected amount of logic for a “declarative” language like CSS.
If you find yourself wrestling with CSS layout, it’s likely you’re making decisions for browsers they should be making themselves. Through a series of simple, composable layouts, Every Layout will teach you how to better harness the built-in algorithms that power browsers and CSS.