CSS flex isn’t flexible, here’s why
UIScript DevBlog, Part 15
Intro
As promised, let’s talk about why flex isn’t actually flexible in most cases.
When we discuss flex, we tend to focus on many properties like the flex-flow
, flex-wrap
and flex-basis
, but almost always forget about two very important properties: the width
and height
of the container.
Take a look at this well-known cheat sheet from CSS-Tricks. It only mentions width
three times, and height
only once, neither is about the size of the container.
When you introduce the width
and height
of the container into the mix, flex suddenly becomes much easier to digest.
Let’s see why.
Size, not flex
All containers, including flex, have only three options to manage sizes:
- set the size of the parent and the size of the children independently
- derive the size of the parent from the size of the children
- derive the size of the children from the size of the parent
Note: The 4th option would be when we derive the size of the parent from the children, while also deriving the size of the children from the parent, which is impossible.
According to CSS Flexible Box Layout Module Level 1, § 7. Flexibility “the defining aspect of flex layout is the ability to make the flex items “flex”, altering their width/height to fill the available space in the main dimension”.
This statement is completely false, for several reasons:
- The defining aspect of this layout model is not that the children “flex”, but that they are distributed along an axis (either horizontally or vertically)
- Children only “flex” in one out of three scenarios
- The parent can also “flex”, but again, only in a single scenario
- We always care about both dimensions, not just the main
Instead of focusing on the “flex” aspect, I suggest that you focus on the sizing aspect. Use these 3 options as the basis of your mental model and always start by picking the one that fits your use case whenever you need a distribution in your layout.
Let’s see some actual examples.
1. Independent sizes
In this scenario, we set the size of the parent and the size of each children independently. This is useful for example for a carousel or a scrollable list:
This model is quite literally everywhere. This is the macOS System Settings with two vertical Stacks:
The key thing here is that if we add/remove children or shrink/grow them, they will not affect the size of their parent. Excessive children will simply overflow the given size and we can crop or scroll them.
Even though neither the parent nor its children are flexible, the layout is still responsive and automatic layout changes are fully under our control.
In UIScript, this would look something like this:
HStack(
View().width(50), //explicit
View().width(50), //explicit
View().width(50) //explicit
)
.width(100); //explicit
2. Parent size from children sizes
In this scenario, we derive the size of the parent from the size of its children. This is useful for example for vertical or horizontal tab bars:
But the Dock in macOS also works in a similar way:
The key thing here is that if we add/remove children or shrink/grow them, the parent will shrink/grow as well. The children do not “flex”.
This is also an easy scenario, because making smaller sizes add up to a bigger size is usually a straightforward process.
In UIScript, this would look something like this:
HStack(
View().width(50), //explicit
View().width(50), //explicit
View().width(50) //explicit
)
.width(children.width); //implicit
3. Children sizes from parent size
In this scenario, we derive the sizes of children from the size of their parent.
This is where the flexibility of children comes into play and this was also the most touted feature of this model, because people saw this as the ultimate layout solution after the failures of flow-based and table-based approaches.
However, this isn’t some ultimate solution either, it is merely one special case of the distribution model.
A good example for this scenario is when we need multiple columns of text to fill the available space equally:
But of course, the distribution doesn’t have to be equal. Here is an example with two columns and a sidebar:
The problem is, deriving the size of children from the size of the parent is the hardest scenario, because unlike adding things up, dividing something up can happen in virtually endless ways, with various consequences that we need to address.
The complexity of this scenario is well beyond the scope of this article, but I’ll try to touch on this in future articles.
In UIScript, this would look something like this:
HStack(
View().width(parent.width / 3), //implicit
View().width(parent.width / 3), //implicit
View().width(parent.width / 3) //implicit
)
.width(1000); //explicit
Necessary complexity
There are several factors that naturally complicate sizing in containers:
- We have only 3 scenarios but we need to consider them in both directions (horizontally and vertically), so in reality we are working with 9 different scenarios.
- We can (or should be able to) control the sizes of children centrally, from the parent, regardless of scenarios or from individual children directly.
- We can nest containers inside each other, making it quite complex to reconcile a top-down sizing (child size from parent size) with a bottom-up sizing (parent size from child size).
Unnecessary complexity
Unfortunately, there are several issues that further and unnecessarily complicate sizing in CSS flex:
- Sizing is mixed up with positioning in the
align-items
andalign-content
properties. Both of these properties should be purely about positioning, they should not alter the size of any children in any way, yet, thestretch
value does exactly that. - There is no
stretch
value forjustify-content
, making vertical and horizontal sizing inconsistent. - There aren’t any properties on the parent to control the width and height of all children centrally. There should be a concept similar to
grid-template-rows
andgrid-template-columns
. - Sizing on the children is also extremely chaotic: we can use the
width
andheight
properties (even with relative units), but there are also properties likeflex-grow
,flex-shrink
, andflex-basis
that greatly convolute them.
In an upcoming article I will discuss why these issues are severe and propose a much better design.
Conclusion
The CSS flex layout model is not about flexibility, but distribution.
Instead of flexibility, we should focus on sizing.
We can set sizes in a container in three ways:
- the parent and children are independent
- the size of the parent is derived from its children, or
- the size of the children are derived from the parent.
Once we use this as the basis of our mental model, creating distributions will become much easier.
There are factors that naturally complicate this model, such as horizontal and vertical sizing at the same time, different control possibilities and container nesting.
There are also factors that unnecessarily complicate this model, such as inconsistent properties, mixed up values and bad terminology.
The goal of UIScript is to ease both, as much as possible. In the coming weeks I will discuss a better design of this model as part of UIScript.
If you like what I’m doing, please clap, comment and share my work with others.
Thank you and stay tuned.