The Switcher
As we set out in Boxes, it’s better to provide suggestions rather than diktats about the way the visual design is laid out. An overuse of @media breakpoints can easily come about when we try to fix designs to different contexts and devices. By only suggesting to the browser how it should arrange our layout boxes, we move from creating multiple layouts to single quantum layouts existing simultaneously in different states.
The flex-basis property is an especially useful tool when adopting such an approach. A declaration of width: 20rem means just that: make it 20rem wide — regardless of circumstance. But flex-basis: 20rem is more nuanced. It tells the browser to consider 20rem as an ideal or “target” width. It is then free to calculate just how closely the 20rem target can be resembled given the content and available space. You empower the browser to make the right decision for the content, and the user, reading that content, given their circumstances.
Consider the following code.
.grid {
display: flex;
flex-wrap: wrap;
}
.grid > * {
width: 33.333%;
}
@media (max-width: 60rem) {
.grid > * {
width: 50%;
}
}
@media (max-width: 30rem) {
.grid > * {
width: 100%;
}
}
The mistake here (aside from not using the logical property inline-size in place of width) is to adopt an extrinsic approach to the layout: we are thinking about the viewport first, then adapting our boxes to it. It’s verbose, unreliable, and doesn’t make the most of Flexbox’s capabilities.
With flex-basis, it's easy to make a responsive Grid-like layout which is in no need of @media breakpoint intervention. Consider this alternative code:
.grid {
display: flex;
flex-wrap: wrap;
}
.grid > * {
flex: 1 1 20rem;
}
Now I'm thinking intrinsically — in terms of the subject elements’ own dimensions. That flex shorthand property translates to "let each element grow and shrink to fill the space, but try to make it about 20rem wide". Instead of manually pairing the column count to the viewport width, I’m telling the browser to generate the columns based on my desired column width. I’ve automated my layout.
As Zoe Mickley Gillenwater has pointed out, flex-basis, in combination with flex-grow and flex-shrink, achieves something similar to an element/container query in that “breaks” occur, implicitly, according to the available space rather than the viewport width. My Flexbox “grid” will automatically adopt a different layout depending on the size of the container in which it is placed. Hence: quantum layout.
Issues with two-dimensional symmetry
While this is a serviceable layout mechanism, it only produces two layouts wherein each element is the same width:
- The single-column layout (given the narrowest of containers)
- The regular multi-column layout (where each row has an equal number of columns)
In other cases, the number of elements and the available space conspire to make layouts like these:
This is not necessarily a problem that needs to be solved, depending on the brief. So long as the content configures itself to remain in the space, unobscured, the most important battle has been won. However, for smaller numbers of subject elements, there may be cases where you wish to switch directly from a horizontal (one row) to a vertical (one column) layout and bypass the intermediary states.
Any element that has wrapped and grown to adopt a different width could be perceived by the user as being “picked out”; made to deliberately look different, or more important. We should want to avoid this confusion.
The solution
#The Switcher element (based on the bizarrely named Flexbox Holy Albatross) switches a Flexbox context between a horizontal and a vertical layout at a given, container-based breakpoint. That is, if the breakpoint is 30rem, the layout will switch to a vertical configuration when the parent element is less than 30rem wide.
In order to achieve this switch, first a basic horizontal layout is instated, with wrapping and flex-grow enabled:
.switcher > * {
display: flex;
flex-wrap: wrap;
}
.switcher > * > * {
flex-grow: 1;
}
The flex-basis value enters the (current) width of the container, expressed as 100%, into a calculation with the designated 30rem breakpoint.
30rem - 100%
Depending on the parsed value of 100%, this will return either a positive or negative value: positive if the container is narrower than 30rem, or negative if it is wider. This number is then multiplied by 999 to produce either a very large positive number or a very large negative number:
(30rem - 100%) * 999
Here is the flex-basis declaration in situ:
.switcher > * {
display: flex;
flex-wrap: wrap;
}
.switcher > * > * {
flex-grow: 1;
flex-basis: calc((30rem - 100%) * 999);
}
A negative flex-basis value is invalid, and dropped. Thanks to CSS’s resilient error handling this means just the flex-basis line is ignored, and the rest of the CSS is still applied. The erroneous negative flex-basis value is corrected to 0 and—because flex-grow is present—each element grows to take up an equal proportion of horizontal space.
Content width
The previous statement, "each element grows to take up an equal proportion of the horizontal space" is true where the content of any one element does not exceed that alloted proportion. To keep things in order, nested elements can be given a max-inline-size of 100%.
min-widths) can be problematic. Instead, width should be suggested or inferred from context.If, on the other hand, the calculated flex-basis value is a large positive number, each element maxes out to take up a whole row. This results in the vertical configuration. Intermediary configurations are successfully bypassed.
Gutters
To support margins ('gutters'; 'gaps') between the subject elements, we could adapt the negative margin technique covered in the Cluster documentation. However, the flex-basis calculation would need to be adapted to compensate for the increased width produced by stretching the parent element. That is, by applying negative margins on all sides, the parent becomes wider than its container and their 100% values no longer match.
.switcher {
--threshold: 30rem;
--space: 1rem;
}
.switcher > * {
display: flex;
flex-wrap: wrap;
/* ↓ Multiply by -1 to make negative */
margin: calc(var(--space) / 2 * -1);
}
.switcher > * > * {
flex-grow: 1;
flex-basis: calc((var(--threshold) - (100% - var(--space))) * 999);
/* ↓ Half the value to each element, combining to make the whole */
margin: calc(var(--space) / 2);
}
Instead, since gap is now supported in all major browsers, we don’t have to worry about such calculations any more. The gap property represents the browser making such calculations for us. And it allows us to cut both the HTML and CSS code down quite a bit.
.switcher {
display: flex;
flex-wrap: wrap;
gap: 1rem;
--threshold: 30rem;
}
.switcher > * {
flex-grow: 1;
flex-basis: calc((var(--threshold) - 100%) * 999);
}
Managing proportions
There is no reason why one or more of the elements, when in a horizontal configuration, cannot be alloted more or less of the available space. By giving the second element (:nth-child(2)) flex-grow: 2 will become twice as wide as its siblings (and the siblings will shrink to compensate).
.switcher > :nth-child(2) {
flex-grow: 2;
}
Quantity threshold
In the horizontal configuration, the amount of space alloted each element is determined by two things:
- The total space available (the width of the container)
- The number of sibling elements
So far, my Switcher switches according to the available space. But we can add as many elements as we like, and they will lay out together horizontally above the breakpoint (or threshold). The more elements we add, the less space each gets alloted, and things can easily start to get squashed up.
This is something that could be addressed in documentation, or by providing warning or error messages in the developer's console. But that isn't very efficient or robust. Better to teach the layout to handle this problem itself. The aim for each of the layouts in this project is to make them as self-governing as possible.
It is quite possible to style each of a group of sibling elements based on how many siblings there are in total. The technique is something called a quantity query. Consider the following code.
.switcher > :nth-last-child(n+5),
.switcher > :nth-last-child(n+5) ~ * {
flex-basis: 100%;
}
Here, we are applying a flex-basis of 100% to each element, only where there are five or more elements in total. The nth-last-child(n+5) selector targets any elements that are more than 4 from the end of the set. Then, the general sibling combinator (~) applies the same rule to the rest of the elements (it matches anything after :nth-last-child(n+5)). If there are fewer that 5 items, no :nth-last-child(n+5) elements and the style is not applied.
Now the layout has two kinds of threshold that it can handle, and is twice as robust as a result.
Use cases
#There are any number of situations in which you might want to switch directly between a horizontal and vertical layout. But it is especially useful where each element should be considered equal, or part of a continuum. Card components advertising products should share the same width no matter the orientation, otherwise one or more cards could be perceived as highlighted or featured in some way.
A set of numbered steps is also easier on cognition if those steps are laid out along one horizontal or vertical line.
The Generator
#Use this tool to generate basic Switcher CSS and HTML.
The Component
#A custom element implementation of the Switcher is provided for download. Consult the API and examples to follow for more information.
Download Switcher.zipProps API
The following props (attributes) will cause the Switcher component to re-render when altered. They can be altered by hand—in browser developer tools—or as the subjects of inherited application state.
| Name | Type | Default | Description |
|---|---|---|---|
| threshold | string |
"var(--measure)" |
A CSS width value (representing the 'container breakpoint') |
| space | string |
"var(--s1)" |
A CSS margin value |
| limit | integer |
4 |
A number representing the maximum number of items permitted for a horizontal layout |