Historically, layout in CSS has always been quite difficult. A major reason is that “normal flow” – the default layout model of the web – is intended for laying out text in documents, not for all the other things we expect today’s web to do.
These days, we have other options for laying things out, such as flexbox and grid. However, normal flow remains the default layout, and the one that is used the most. As such, I think it’s worth understanding normal flow, what it’s good for, and why it makes it so hard to centre things vertically. In order to do that though, there are three main concepts that we need to understand – block elements, inline elements, and line boxes.
Etsy. Definitely not relying on normal flow.
Block elements are elements which contain text, or other block elements. For example, the browser renders,
<ul> as block elements by default (although you can force any element to render as a block element by giving it a style of display: block). The most basic block element is
<div>, which is essentially defined as a block element with no semantics attached.
Block elements are laid out the same way paragraphs are laid out. If your page is in English (or any other language that uses the same alphabet as English), this means they flow vertically down the screen, with each block element appearing directly below the previous one. If your page uses a vertical script, like traditional Japanese, then the blocks are laid out side-by-side. In the case of Japanese, each block would appear immediately to the left of the previous one. The direction that the block elements flow in is referred to as the “block direction”.
Left: French text, with a block direction going down the page; Right: Japanese text, with a block direction going across the page, from right to left
(In reality, for most web pages, the block direction runs down the page. Even most Japanese pages are actually written this way.)
An article on Nintendo’s Japanese site, written left to right, top to bottom, just like English
By default, a block element is going to be the same width as its parent container, and as tall as it needs to be to fit its contents (with padding). We can set the width manually, and the height will automatically adjust to fit the content at this width. We can also set the height manually. But if we set the width and height to be too small to fit the content, then the content will overflow from the container.
Top: <div> with no width or height set takes 100% of parent width and height required to fit content; Bottom-left: <div> with width set to 50%, height increases to fit content; Bottom-right: <div> with width set to 50% and height set to 150px, content overflows
We can also set paddings and margins on block elements, to add a bit of space to our design. Setting margin: auto on a block level element will centre it horizontally, and remove the vertical margin. This makes sense if you remember that we’re laying out text. Centering a heading or paragraph horizontally is a fairly common use case. But, we don’t usually centre paragraphs vertically – instead, we group them near the start of the page.
Element with width: 50% and margin: auto is centred horizontally, but has no vertical margin
One small trick with block elements is margin-collapse. If you have two adjacent block elements, their margins will overlap, and the gap between them will be the same size as the larger margin (rather than the two margins combined). Again, this makes sense in the context of laying out text, as it simplifies standardising the gaps between headings and paragraphs.
Text with margins highlighted. The <h2> element (“CHAPITRE II”) has a 2em margin, top and bottom, highlighted in yellow. The <p> elements have 1em margins, top and bottom, highlighted in pink. Notice that the bottom margin of the <h2> overlaps with the top margin of the first <p>, and the bottom margin of each <p> overlaps with the top margin of the <p> below it. This allows us to use consistent margins on elements. For example, we don’t need to remove the top margin from the first paragraph to keep the spacing between it and the heading correct, or give all the <p> elements half-sized margins and then somehow work out what to do when there’s an image.
If you would like to read more about margin-collapse (and how to stop it happening, if you need to), check out Rachel Andrew’s article “Everything You Need To Know About CSS Margins”.
Inline elements are the actual text of our text layout. The browser renders <em>, <strong>, <b>, <i>, and <a> elements inline by default (although, as with block elements, you can make any element render inline by giving it a style of display: inline). The basic inline element is <span>. Additionally, text that is not inside any element is rendered inline.
Inline elements are a little bit trickier to work with than block elements. For a start, the width and height of the element are determined entirely by the content, and can’t be set manually. And while it is possible to set paddings and margins, they only have an effect in the horizontal direction. And setting margin: auto on an inline element doesn’t centre it – it just removes all margins. You can, however, centre inline elements by setting text-align: center on the parent element.
The highlighted text is a with padding: 0.5em. Notice that the padding is actually added to all four sides, but it overlaps the adjacent text above and below, and only really creates space horizontally.
Just like block elements flow in the block direction, inline elements flow in the inline direction. For a page in English, this means left-to-right. For a page using Arabic script, this would mean right-to-left. And for a page in traditional Japanese, this would be top-to-bottom.
To really understand how inline elements are laid out though, we first need to understand line boxes.
Line boxes are used by the browser to determine how to lay out inline elements inside of a container element. They’re kind of invisible to developers, but we can think of them like a smaller box inside the container. To begin with, the container has a single line box. The line box is the same width as its container, but has no height. It’s located at the top of the container, just inside the container’s padding.
Line box, with nothing in it. It looks more like a line than a box because it has a height of 0px.
The container’s content is added to the line box, in the inline direction. As the content is added, the line box’s height is adjusted, so it’s always the same height as the tallest content.
Top: The height of the line box is the same as the height of its content. Bottom: If we add some taller content, the line box increases its height to fit the new content
Once the first line box is full, a second one is added, directly below the first one. The most recently added element will be split across the two line boxes. The browser will try to fill the entire line box, and split the element wherever there is whitespace. If there’s no whitespace available, then the element (and line box) will overflow the container. For more information about controlling how elements are broken over line boxes, have a look at word-break and overflow-wrap.
Top: When the line box is full, a new line box is created below it; Bottom: Content can only be broken on whitespace. If there’s no whitespace, the content will overflow the container.
If the container has a block element child, it doesn’t get a line box. It goes on a line by itself, then a new line box is created beneath it.
Influencing line boxes
Something to note about the height of line boxes is that they are always calculated based only on the content of that particular line box. This means that a container could have line boxes of multiple different sizes.
While this example doesn’t show the line boxes, you can see how the lines are all different sizes, making the flow of text uneven.
CSS currently gives us very little control over line boxes and how things are laid out in them, but we do have a couple of tools available. The first of these is the line-height property. If we add a line-height to the container, then this sets a minimum height for all line boxes inside the container. Line-height can be set using an absolute value, like 24px, or a relative value. The relative value can be set as a percentage (150%), or a unitless value (1.5). In both cases, it sets the line-height relative to the font size of the container. Relative values are generally preferred, so that the line-heights still work correctly, even if the font size is changed.
This container has line-height: 1.5em, ensuring that each line box is at least 1.5em tall. This gives the text a more consistent flow, as all of the lines are the same height.
We can also set the line-height of an individual element inside the container. If we do this, then the line-height of the element (rather than its actual height) is used when calculating the height of the line box. Be careful when doing this. If we set the line-height smaller than the actual height, then the element will overflow into adjacent line boxes.
Here, the larger font has font-size: 1.3em and line-height: 0.9em. You can see it overlaps both the smaller text above, and the larger text below. This isn’t as much of a problem as it might seem at first though, because fonts typically have a bit of whitespace built into them.
We can also control the placement of an element relative to the line box, using the vertical-align property. We can align the element with the top or bottom of the line box, or with various parts of the font. For example, vertical-align: baseline should align the elements so it looks like all the text was written on the same horizontal line. Just beware when using vertical-align: middle that it doesn’t refer to the middle of the line box. It refers to the middle of the font (technically, the baseline + half the x-height).
Here, the font-size of the text is 40px, while the font-size of the rocket emojis is only 20px, ensuring the line-height is larger than the height of the emojis.
The most important thing to remember about vertical-align is that it will only work with inline elements, when the element is a different height to its line box. If you’re trying to align something vertically in any other context, you probably don’t want to use normal flow.
Top button: display: block; text-align: center; vertical-align: baseline. Notice how the text is not centred vertically. Bottom button, using flex layout rather than normal flow: display: flex; align-items: baseline; justify-content: center; Aligned just right!
For a more in-depth discussion of line-height and all its intricacies, have a look at How to Tame Line Height in CSS on CSS Tricks.
The last thing I want to mention here is `display: inline-block` which looks like a complete contradiction in terms. Inline-block elements are laid out inline, in a line box, while still allowing us to control the width and height of the element, and add vertical padding and margins. They’re handy if you want to place something like a button or image inline with some text, while still being able to style it like a block element. If you’re trying to do anything more complicated than that, then I would suggest trying a flexbox or grid layout instead.
The SVG icon here is an example of a good use of inline-block. We want it to display inline with the text, but we also need to be able to control its size.
What have we learned?
Hopefully by this point, you’ve got a much better understanding of how normal flow works, and why it’s so hard to centre things vertically! Just remember, normal flow is intended for laying out text inside documents. While it’s a decent default in most situations, we do have other alternatives available now, and they might suit your particular needs much better.