JoT
Flashcard
learning

CSS: Flexbox and the Box Model

2023-07

Huidong Yang

Flexbox

In a way, it's easier to learn about Flexbox after knowing Elm-UI. Heck, I also believe it's way better to learn Elm before JavaScript, if your goal is to write full-fledged apps instead of simpler websites. Solid mental models are more important than syntax.

Elm-UI is implemented using Flexbox, but the concepts (similar but simplified), terminology, and API, are all better, and therefore, already knowing Elm-UI can serve as a learning guide, and essentially, you just ask the question, How do I get the Elm-UI constructs from writing Flexbox code?


No doubt, the most used Elm-UI layout constructs are row and column, containers that can have multiple children. In Flexbox, we use flex-direction to specify which layout orientation we want (row is the default), on a flex container (i.e. display:flex). This orientation defines the "main axis", while the orthogonal direction is referred to as the "cross axis".

Sidenote: there's a third layout construct in Elm-UI: el, which contains exactly one child. You might wonder why having this since we already have the more general-purpose row/column. My understanding is as follows:

  • obviously, for explicitness and better constraint: instead of a List of children (could be zero or more), we often need to say, there must be one and only one thing inside;
  • Elm-UI purposefully eliminated margin, so in order to create a margin outside certain object of interest, we need to wrap it with an el and use its padding to emulate the margin effect.

The second most used constructs (in my opinion of course) is shrink and fill (plus fillPortion), and everything is shrink by default. In Flexbox, similar constructs exist, i.e. flex-shrink (default: 1) and flex-grow (default: 0), and you can specify arbitrary numbers to define proportionality relationships among sibling items in a flex container, so it's easy to identify much resemblance between the two systems. Nevertheless, the differences are also significant. In Elm-UI, the two constructs can be used with either height or width, on any sizable container (el, row, column); whereas in Flexbox, these counterpart properties only have effect along the main axis, and you would need to use a different set of properties to achieve similar effects along the cross axis, e.g. align-items:stretch on the flex container (or align-self on individual items within).

So this is a huge discrepancy API-wise, and you may wonder why. Elm-UI makes it nice and symmetrical regardless of the direction along which you're specifying the sizing preferences, while in Flexbox, the two directions are treated very differently. Now if you think about it, things are different: say we're in a column, and along the main (vertical) axis, things are stacked on top of one another, and as a result, when you specify shrink/grow, they'll compete, pushing each other, and that's why we need to specify those proportionality numbers for individual items; on the contrary, along the cross (horizontal) axis, the items are completely independent, even if wrapping is enabled (via flex-wrap:wrap). As a result, it makes no sense to specify a number when it comes to expanding along the cross axis, instead, you simply say stretch.

Therefore, with Elm-UI, you could write, say within a row, one item has height (fillPortion 1), another height (fillPortion 2), which doesn't make sense at all, since the two are not interacting along the vertical axis at all, but unfortunately, here Elm-UI can't prevent you from speaking the nonsense. So I'd say, in this case, the Flexbox API actually provides more sensible constraints in this regard. But at what cost? A cognitive load significantly heavier than a simple word matrix that is height/width x shrink/fill.

Sidenotes:

  • In Elm-UI, both width and height are shrink by default, instead of fill; whereas in Flexbox, the defaults are set differently depending on the axis: along the main axis, flex-grow is disabled, and flex-shrink is enabled (1), so items don't fill the available space by default; on the other hand, along the cross axis, align-items is stretch by default, so they fill empty space by default; although this may seem inconsistent, the decision could come from practical considerations, for instance, default fill/stretch along the cross axis results in all items being well-aligned per "line" instead of being ragged, regardless of individual sizes, whereas along the main axis, the list of items are stacked next to one another, and it would be too strong of an opinion to make all of them grow equally by default (e.g. we don't want all of header, main, and footer to grow equally and fill the vertical space, typically we just want the main to do so), but shrinking when there's not enough space is a safer bet for default behavior.

  • "shrink" in Elm-UI and "flex-shrink" in Flexbox actually have different meanings! In Elm-UI, it simply means the object's dimension will take that of of its content, so it's more like "shrinkwrap"; In Flexbox however, it's actually going to reduce the size of the targeted object if the container is not large enough to hold all the items, and "flex-shrink" takes arbitrary numbers for proportionality, whereas in Elm-UI, there's only "fillPortion", but no such thing as "shrinkPortion".


In terms of alignment and arrangement/distribution, again Elm-UI takes a simplified approach, whereas Flexbox seems more "industrial-strength", at the cost of a more complex API.

With Elm-UI, it's dead simple. Alignment is just center{X,Y}, and align{Top,Bottom,Left,Right}; Do note that it doesn't use "Start"/"End" instead, which could mean that RTL support is not as modern as in Flexbox; The spacing/distribution API is also very minimal, there's spaceEvenly, or you specify spacing explicitly with a pixel size.

With Flexbox, things are a bit muddy, because "alignment" and "arrangement" are mixed together; You might think that justify-* properties are for only arrangement/distribution, whereas align-* are for alignment only; not quite.

It's more like, "justify" specifically works on the main axis, and is for both arrangement (space-{evenly,between,around}) and alignment (flex-{start,end}, center), and in Flexbox realm, only justify-content matters, whereas justify-items isn't applicable; then we have "align", which works on the cross axis, but this time, things are more complicated. Both align-items and align-content are meaningful in Flexbox, but the latter only has effect when the container is wrapped, aka. "multiline", in which case it specifies how those "lines" of items as a whole should be aligned or arranged (see "justify-content" above for options, plus stretch, which behaves like "space-*" but fills available space); In practice though, for GUI elements, a wrapped layout is not typically used (in my own experience for instance, Elm-UI's wrappedRow is only used for content-oriented components, like a list of tags, and there's no "wrappedColumn" at all). Therefore, "align-items" is what we care about in most cases when it comes to the cross axis, and it works on the items per-line, applicable in both single- and multiple-line cases (cf. "align-content", which only works over multiple lines). Note that the default value of "align-items" is "stretch", and the options are alignment only, that is, we no longer have those "space-*" options! And this actually makes sense if you think about it, for the idea of item distribution along the cross axis within a line is nonsensical (there is only one item per line, duh).

Note that we can also override the global "align-items" settings per individual item, using align-self; however, justify-self is again N/A in Flexbox, because, as the items along the main axis are interacting with one another, individual overrides per item can break the arrangement of other items defined by the global "justify-content" property; for example, say we have a row of three items, all aligned as "flex-start", and then we override the first one to be "center", then should the next two be also pushed to the center? If so, then why not simply use "center" as the global setting? So instead of trying to define a myriad of such "interaction behaviors" when overriding occurs, Flexbox decides to simply not allow it. Now in contrast, since the alignment of items along the cross axis is independent of one another, overriding the settings of any number of individual items does not affect the behavior of the remaining items that are taking the default config from the parent container.

The Box Model

From inside out, we have content < padding < border < margin, but margin isn't really part of a "box" per se, and thus excluded from its size computation. Border is, naturally, the boundary of the real thing. Margin, on the other hand, can be thought of as a repelling "field" that prevents other boxes from coming any closer than its set distance; and that helps us understand the phenomenon known as "margin collapsing", where two adjacent boxes A and B, with margins a and b, respectively, will not be spaced by a+b, but rather, max(a,b). Here's how we may reason about this: box A ensures that B can't come closer than distance a, and B ensures that A can't come closer than distance b, so whichever repelling field that is farther-reaching, will stop the neighboring box from getting closer first, hence that larger margin will determine the resulting spacing between the two; basically, if we conceptualize margin as a "non-material" field that only acts on materials (i.e. boxes bounded by their borders), but not on other fields, then this collapsing will make sense.


Now when it comes to determining the size of a box, by default, it only takes the element content area into account, excluding the padding and the border thickness, and in GUI making, this is always never desired; therefore, most likely you want to globally override the default by specifying box-sizing:border-box, as opposed to the default value content-box.


display:inline-box is useful when you want to turn an inline-by-default element (like a link) into something that acts more like a "block", but one that doesn't take a whole line on its own; here's what we mean by being "more like a block":

  • its size can be reliably specified by width and height (an inline box's width and height values are ignored),
  • its margin, border, and padding will reliably push surrounding elements away, preventing overlap (an inline box's top and bottom margin/border/padding will not do such, but its left and right counterparts will),

Nevertheless, an inline-block box differs from a regular block box in yet another subtle way (besides the obvious part of being inline):

When its size are not explicitly defined, an inline-block box takes only the necessary space and no more (i.e. everything within the border), but a regular block box will, for instance, take the entire available horizontal space if its width is unspecified.


One question is, with Flexbox being mighty and powerful, do we still need this "inline-block" thing? I think the answer is yes, if you need to deal with inline-by-default elements, even when you're working inside a flex container. As we mentioned, an inline box's padding/margin, etc are not honored by the surrounding elements, which causes unwanted overlap. That can't be fixed by Flexbox.