While watching this Kevin Powell video (starting at 4:15), I found out about a really cool CSS pattern for styling prose (as in article text, just like what you're reading right now). More specifically, it's about defining the space between items. It's based on Tailwind's "prose" styling and uses the lobotomized owl selector (* + *).
The Lobotomized Owl 🦉
The name was introduced by Heydon Pickering back in 2014, and it's an awesome name for a very straightforward selector.
The lobotomized owl (* + *) will select any element that is not the first, which makes it basically the same as *:not(:first-child).
The Prose
Let's assume we have a blog post that has its content rendered like this:
html
<div class="prose"> <h2>Lorem ipsum dolor</h2> <p>Ad officia consequat pariatur.</p> <p>Mollit eu anim qui qui et labore sit tempor sit.</p> <h2>Fugiat laboris esse mollit</h2> <p>Ad ipsum dolore laboris nisi</p> </div>
How would you control the spacing between each piece of the text? You could either set a flat spacing (by setting .prose a flexbox and giving it a gap, among other ways), or target any child and give it a predefined margin-top depending on what type it is (so it's larger on headings, for example).
An extremely elegant way of doing that, though, is by using em, which will make the margin vary by the font-size of an element!
css
h2 { font-size: 2rem; } p { font-size: 1rem; } .prose > * + * { margin-top: 1.4em; }
The CSS above sets a margin-top to any element (except the first) based on the text size of that element (because of em). So on a <p> with 16px font size, margin-top would be 16px * 1.4 = 22.4px. On an h2 with 32px font size, it'd be 44.8px.
A refresher on em vs rem
It's not uncommon to get these two units confused — but it's important to understand their differences or you might end up with some pretty nasty debugging challenges.
Both scale off of font-size, what changes is what font-size they use. rem will always refer to the font-size of the body element, which defaults to 16px if not set. This means that it is a very predictable unit. 1rem will always be the same value no matter which element uses it.
em, on the other hand, scales off of the font-size of the current element. This means that 1em inside a heading tag can be very different from 1em in a footnotes section.
Both have their uses and can coexist, as we can see right now.
This sets a nice default spacing for all possible elements inside of your .prose div. But you can also use the same idea to handle special cases. Let's say you always want your images to have bigger margin around them. We can target them specifically:
css
.prose > * + img { margin-top: 2.8em; margin-bottom: 2.8em; }
I am adding a margin-bottom for that one as well so that the spacing remains consistent. Since margins don't stack in CSS, this means that even if the element below the image has a margin-top, it will merge with the image's margin-bottom. In practice, only the biggest margin counts.
Pro tip: use a variable with a fallback
A really good practice when doing stuff like this is to use CSS variables with a fallback value, so you can easily override the default value whenever you need.
Consider your .prose styling has a default spacing of 1.4em, as we've established above. But now you want to use that same styling in a card component. Since on that card you have less space to work with, it'd be ideal if the spacing was a bit tighter than normal. You could do something like:
css
.prose > * + * { margin-top: var(--prose-spacing, 1.4em); } .prose > * + img { margin-top: calc(var(--prose-spacing, 1.4em) * 2); margin-bottom: calc(var(--prose-spacing, 1.4em) * 2); } .card .prose { --prose-spacing: 1.2em; }
Pro Max tip: use round
If the decimal values (like margin being 22.4px) bother you, you can use the CSS round function to make sure it maps to a predictable, integer value.
If you're interested, I highly recommend reading this amazing article by Ahmad Shadeed about the round function, but the gist of it is that you can do something like:
css
h2 { font-size: 2rem; } p { font-size: 1rem; } .prose > * + * { margin-top: round(var(--prose-spacing, 1.4em), 2px); }
Wrapping up
This kind of elegant, simple approach is one of my favorite things about CSS and web development in general. We can easily over-engineer things, but nothing beats something like this.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.