Tag: Spacing

Solved With :has(): Vertical Spacing in Long-Form Text

If you’ve ever worked on sites with lots of long-form text — especially CMS sites where people can enter screeds of text in a WYSIWYG editor — you’ve likely had to write CSS to manage the vertical spacing between different typographic elements, like headings, paragraphs, lists and so on.

It’s surprisingly tricky to get this right. And it’s one reason why things like the Tailwind Typography plugin and Stack Overflow’s Prose exist — although these handle much more than just vertical spacing.

Firefox supports :has() behind the layout.css.has-selector.enabled flag in about:config at the time of writing.

What makes typographic vertical spacing complicated?

Surely it should just be as simple as saying that each element — p, h2, ul, etc. — has some amount of top and/or bottom margin… right? Sadly, this isn’t the case. Consider this desired behavior:

  • The first and last elements in a block of long-form text shouldn’t have any extra space above or below (respectively). This is so that other, non-typographic elements are still placed predictably around the long-form content.
  • Sections within the long-form content should have a nice big space between them. A “section” being a heading and all the following content that belongs to that heading. In practice, this means having a nice big space before a heading… but not if that heading is immediately preceded by another heading!
Example of a Heading 3 following a paragraph and another following a Heading 2.
We want to more space above the Heading 3 when it follows a typographic element, like a paragraph, but less space when it immediately follows another heading.

You need to look no further than right here at CSS-Tricks to see where this could come in handy. Here are a couple of screenshots of spacing I pulled from another article.

A Heading 2 element directly above a Heading 3.
The vertical spacing between Heading 2 and Heading 3
A Heading 3 element directly following a paragraph element.
The vertical space between Heading 3 and a paragraph

The traditional solution

The typical solution I’ve seen involves putting any long-form content in a wrapping div (or a semantic tag, if appropriate). My go-to class name has been .rich-text, which I think I use as a hangover from older versions of the Wagtail CMS, which would add this class automatically when rendering WYSIWYG content. Tailwind Typography uses a .prose class (plus some modifier classes).

Then we add CSS to select all typographic elements in that wrapper and add vertical margins. Noting, of course, the special behavior mentioned above to do with stacked headings and the first/last element.

The traditional solution sounds reasonable… what’s the problem?

Rigid structure

Having to add a wrapper class like .rich-text in all the right places means baking in a specific structure to your HTML code. That’s sometimes necessary, but it feels like it shouldn’t have to be in this particular case. It can also be easy to forget to do this everywhere you need to, especially if you need to use it for a mix of CMS and hard-coded content.

The HTML structure gets even more rigid when you want to be able to trim the top and bottom margin off the first and last elements, respectively, because they need to be immediate children of the wrapper element, e.g., .rich-text > *:first-child. That > is important — after all, we don’t want to accidentally select the first list item in each ul or ol with this selector.

Mixing margin properties

In the pre-:has() world, we haven’t had a way to select an element based on what follows it. Therefore, the traditional approach to spacing typographic elements involves using a mix of both margin-top and margin-bottom:

  1. We start by setting our default spacing to elements with margin-bottom.
  2. Next, we space out our “sections” using margin-top — i.e. very big space above each heading
  3. Then we override those big margin-tops when a heading is followed immediately by another heading using the adjacent sibling selector (e.g. h2 + h3).

Now, I don’t know about you, but I’ve always felt it’s better to use a single margin direction when spacing things out, generally favoring margin-bottom (that’s assuming the CSS gap property isn’t feasible, which it is not in this case). Whether this is a big deal, or even true, I’ll let you decide. But personally, I’d rather be setting margin-bottom for spacing long-form content.

Collapsing margins

Because of collapsing margins, this mix of top and bottom margins isn’t a big problem per se. Only the larger of two stacked margins will take effect, not the sum of both margins. But… well… I don’t really like collapsing margins.

Collapsing margins are yet one more thing to be aware of. It might be confusing for junior devs who aren’t up to speed with that CSS quirk. The spacing will totally change (i.e. stop collapsing) if you were to change the wrapper to a flex layout with flex-direction: column for instance, which is something that wouldn’t happen if you set your vertical margins in a single direction.

I more-or-less know how collapsing margins work, and I know that they’re there by design. I also know they’ve made my life easier on occasion. But they’ve also made it harder other times. I just think they’re kinda weird, and I’d generally rather avoid relying on them.

The :has() solution

And here is my attempt at solving these issues with :has().

To recap the improvements this aims to make:

  • No wrapper class is required.
  • We’re working with a consistent margin direction.
  • Collapsing margins are avoided (which may or may not be an improvement, depending on your stance).
  • There’s no setting styles and then immediately overriding them.

Notes and caveats on the :has() solution

  • Always check browser support. At time of writing, Firefox only supports :has() behind an experimental flag.
  • My solution doesn’t include all possible typographic elements. For instance, there’s no <blockquote> in my demo. The selector list is easy enough to extend though.
  • My solution also doesn’t handle non-typographic elements that may be present in your particular long-form text blocks, e.g. <img>. That’s because for the sites I work on, we tend to lock down the WYSIWYG as much as possible to core text nodes, like headings, paragraphs, and lists. Anything else — e.g. quotes, images, tables, etc. — is a separate CMS component block, and those blocks themselves are spaced apart from each other when rendered on a page. But again, the selector list can be extended.
  • I’ve only included h1 for the sake of completeness. I usually wouldn’t allow a CMS user to add an h1 via WYSIWYG, as the page title would be baked into the page template somewhere rather than entered in the CMS page editor.
  • I’m not catering for a heading followed immediately by the same level heading (h2 + h2). This would mean that the first heading wouldn’t “own” any content, which seems like a misuse of headings (and, correct me if I’m wrong, but it might violate WCAG 1.3.1 Info and Relationships). I’m also not catering for skipped heading levels, which are invalid.
  • I am in no way knocking the existing approaches I mentioned. If and when I build another Tailwind site I’ll use the excellent Typography plugin, no question!
  • I’m not a designer. I came up with these spacing values by eyeballing it. You probably could (and should) use better values.

Specificity and project structure

I was going to write a whole big thing here about how the traditional method and the new :has() way of doing it might fit into the ITCSS methodology… But now that we have :where() (the zero-specificity selector) you can pretty much choose your preferred level of specificity for any selector now.

That said, the fact that we’re no longer dealing with a wrapper — .prose, .rich-text, etc. — to me makes it feel like this should live in the “elements” layer, i.e. before you start dealing with class-level specificity. I’ve used :where() in my examples to keep specificity consistent. All the selectors in both of my examples have a specificity score of 0,0,1 (except for the bare-bones reset).

Wrapping up

So there you have it, a bleeding-edge solution to a very boring problem! This newer approach is still not what I’d call “simple” CSS — as I said at the beginning, it’s a more complex topic than it might seem at first. But aside from having a few slightly complex selectors, I think the new approach makes more sense overall, and the less rigid HTML structure seems very appealing.

If you end up using this, or something like it, I’d love to know how it works out for you. And if you can think of ways to improve it, I’d love to hear those too!

Solved With :has(): Vertical Spacing in Long-Form Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , , ,

How Do You Handle Component Spacing in a Design System?

Say you’ve got a <Card /> component. It’s highly likely it shouldn’t be butted right up against any other components with no spacing around it. That’s true for… pretty much every component. So, how do you handle component spacing in a design system?

Do you apply spacing using margin directly on the <Card />? Perhaps margin-block-end: 1rem; margin-inline-end: 1rem; so it pushes away from the two sides where more content natural flows? That’s a little presumptuous. Perhaps the cards are children inside a <Grid /> component and the grid applies a gap: 1rem. That’s awkward, as now the <Card /> component spacing is going to conflict with the <Grid /> component spacing, which is very likely not what you want, not to mention the amount of space is hard coded.

Example of a component spacing where a card component is to the left of an accordion component and above an article, with 50 pixels of spacing between all three elements. Lorem i-sum text throughout in a mono font. The card has a Calvin and Hobbes comic image.
Adding space to the inline start and block end of a card component.

Different perspectives on component spacing

Eric Bailey got into this recently and looked at some options:

  • You could bake spacing into every component and try to be as clever as you can about it. (But that’s pretty limiting.)
  • You could pass in component spacing, like <Card space="xxl" />. (That can be a good approach, likely needs more than one prop, maybe even one for each direction, which is quite verbose.)
  • You could use no component spacing and create something like a <Spacer /> or <Layout /> component specifically for spacing between components. (It breaks up the job of components nicely, but can also be verbose and add unnecessary DOM weight.)

This conversation has a wide spectrum of viewpoints, some as extreme as Max Stoiber saying just never use margin ever at all. That’s a little dogmatic for me, but I like that it’s trying to rethink things. I do like the idea of taking the job of spacing and layout away from components themselves — like, for example, those content components should completely not care where they are used and let layout happen a level up from them.

Adam Argyle predicted a few years back that the use of margin in CSS would decline as the use of gap rises. He’s probably going to end up right about this, especially now that flexbox has gap and that developers have an appetite these dats to use CSS Flexbox and Grid on nearly everything at both a macro and micro level.

How Do You Handle Component Spacing in a Design System? originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , , ,

Consistent, Fluidly Scaling Type and Spacing

When Chris first sent me this prompt, I was thinking about writing about progressive enhancement, but that subject is so wide-reaching to be one thing and all too predictable, especially for those already familiar with my writing. Saying that, what I’m going to outline in this article also isn’t just one thing either, but the day I meet a writing prompt exactly will be the day pigs start flying. This one group of things, though, will change how you write CSS.

I personally think this group of things lets a lot of sites down—especially in a responsive design sense. The things in this group are typography and spacing. So often, I see inconsistent spacing—especially vertically—that makes content hard to scan and creates this subtle, disjointed feeling. The same goes for type: huge headings on small viewports, or heading hierarchies that visually have no contrast in size, rendering them useless in a visual sense.

There is a pretty easy fix for all of this using a sizing scale and fluid type, and I promise it’ll make your websites look and feel heaps better. Let’s get into it.

What the heck is a sizing scale?

A sizing scale is a uniform progression of sizes based on a scale—or, more accurately, a ratio.

Screensjhot of the type-scale.com type scale tool. It displays eight variations of font sizes in black on a white background starting from largest to smallest vertically. To the left of the examples are options to configure the output, including base font size, type of scale, Google font selection, and preview text.

In that screenshot of type-scale.com, I’ve selected a “Perfect Fourth” scale which uses a ratio of 1.333. This means each time you go up a size, you multiply the current size by 1.333, and each time you go down a size, you subtract 1.333.

If you have a base font size of 16px, using this scale, the next size up is 16 * 1.333, which is 21.33px. The next size up is 21.33 * 1.333, which is 28.43px. This provides a lovely curve as you move up and down the scale.

CSS clamp() and type fluidity

For years, if you were to say, “Hey Andy, what’s your favorite CSS feature?” I would immediately say flexbox, but nah, not these days. I am a [clamp()](https://developer.mozilla.org/en-US/docs/Web/CSS/clamp()) super fan. I wrote about it in more detail here, but the summary of clamp() is that it does clever stuff based on three parameters you give it:

  • a minimum value
  • an ideal value
  • a maximum value

This makes for a very useful tool in the context of fluid typography and spacing, because you write CSS like this:

.my-element {   font-size: clamp(1rem, calc(1rem * 3vw), 2rem); }

This tiny bit of CSS gives us full responsive text sizes based on the viewport width with handy locks to make sure sizes don’t get too big or too small.

It’s really important to test that your text is legible when you zoom in and zoom out when using clamp. It should be very obviously larger or smaller. Because we’re using a rem units as part of our fluid calculation, we’re helping that considerably.

Putting it all together

Right, so we’ve got a size scale and CSS clamp() all set up—how does it all come together? The smart people behind Utopia came up with the simplest, but handiest of approaches. I use their type tool and their spacing tool to create size scales for small and large viewports. Then, using clamp(), I generate a master size scale that’s completely fluid, as well as a Sass map that informs Gorko’s configuration.

$ gorko-size-scale: (   '300': clamp(0.7rem, 0.66rem + 0.2vw, 0.8rem),   '400': clamp(0.88rem, 0.83rem + 0.24vw, 1rem),   '500': clamp(1.09rem, 1rem + 0.47vw, 1.33rem),   '600': clamp(1.37rem, 1.21rem + 0.8vw, 1.78rem),   '700': clamp(1.71rem, 1.45rem + 1.29vw, 2.37rem),   '800': clamp(2.14rem, 1.74rem + 1.99vw, 3.16rem),   '900': clamp(2.67rem, 2.07rem + 3vw, 4.21rem),   '1000': clamp(3.34rem, 2.45rem + 4.43vw, 5.61rem) );

This snippet is from my site, piccalil.li, and the typography is super simple to work with because of it.

You could also translate that into good ol’ CSS Custom Properties:

:root {   --size-300: clamp(0.7rem, 0.66rem + 0.2vw, 0.8rem);   --size-400: clamp(0.88rem, 0.83rem + 0.24vw, 1rem);   --size-500: clamp(1.09rem, 1rem + 0.47vw, 1.33rem);   --size-600: clamp(1.37rem, 1.21rem + 0.8vw, 1.78rem);   --size-700: clamp(1.71rem, 1.45rem + 1.29vw, 2.37rem);   --size-800: clamp(2.14rem, 1.74rem + 1.99vw, 3.16rem);   --size-900: clamp(2.67rem, 2.07rem + 3vw, 4.21rem);   --size-1000: clamp(3.34rem, 2.45rem + 4.43vw, 5.61rem); };

This approach also works for much larger sites, too. Take the new web.dev design or this fancy software agency’s site. The latter has a huge size scale for large viewports and a much smaller, more sensible, scale for smaller viewports, all perfectly applied and without media queries.

I’m all about keeping things simple. This approach combines a classic design practice—a sizing scale—and a modern CSS feature—clamp()—to make for much simpler CSS that achieves a lot.


, , , ,