If you happen to’ve ever labored on websites with plenty of long-form textual content — particularly CMS websites the place individuals can enter screeds of textual content in a WYSIWYG editor — you’ve doubtless needed to write CSS to handle the vertical spacing between totally different typographic components, like headings, paragraphs, lists and so forth.
It’s surprisingly tough to get this proper. And it’s one motive why issues just like the Tailwind Typography plugin and Stack Overflow’s Prose exist — though these deal with way more than simply vertical spacing.
Firefox helps :has() behind the format.css.has-selector.enabled flag in about:config on the time of writing.
What makes typographic vertical spacing difficult?
Absolutely it ought to simply be so simple as saying that every component — p, h2, ul, and many others. — has some quantity of prime and/or backside margin… proper? Sadly, this isn’t the case. Contemplate this desired conduct:
The primary and final components in a block of long-form textual content shouldn’t have any further house above or under (respectively). That is in order that different, non-typographic components are nonetheless positioned predictably across the long-form content material.
Sections inside the long-form content material ought to have a pleasant massive house between them. A “part” being a heading and all the next content material that belongs to that heading. In apply, this implies having a pleasant massive house earlier than a heading… however not if that heading is instantly preceded by one other heading!
We need to more room above the Heading 3 when it follows a typographic component, like a paragraph, however much less house when it instantly follows one other heading.
It is advisable to look no additional than proper right here at CSS-Methods to see the place this might come in useful. Listed here are a few screenshots of spacing I pulled from one other article.
The vertical spacing between Heading 2 and Heading 3
The vertical house between Heading 3 and a paragraph
The normal resolution
The everyday resolution I’ve seen entails placing any long-form content material in a wrapping div (or a semantic tag, if acceptable). My go-to class identify has been .rich-text, which I believe I take advantage of as a hangover from older variations of the Wagtail CMS, which might add this class robotically when rendering WYSIWYG content material. Tailwind Typography makes use of a .prose class (plus some modifier lessons).
Then we add CSS to pick out all typographic components in that wrapper and add vertical margins. Noting, after all, the particular conduct talked about above to do with stacked headings and the primary/final component.
The normal resolution sounds affordable… what’s the issue?
Inflexible construction
Having so as to add a wrapper class like .rich-text in all the precise locations means baking in a selected construction to your HTML code. That’s generally mandatory, however it feels prefer it shouldn’t need to be on this explicit case. It can be simple to overlook to do that in all places you have to, particularly if you have to use it for a mixture of CMS and hard-coded content material.
The HTML construction will get much more inflexible if you need to have the ability to trim the highest and backside margin off the primary and final components, respectively, as a result of they must be quick youngsters of the wrapper component, e.g., .rich-text > *:first-child. That > is essential — in spite of everything, we don’t need to unintentionally choose the primary listing merchandise in every ul or ol with this selector.
Mixing margin properties
Within the pre-:has() world, we haven’t had a solution to choose a component primarily based on what follows it. Due to this fact, the standard method to spacing typographic components entails utilizing a mixture of each margin-top and margin-bottom:
We begin by setting our default spacing to components with margin-bottom.
Subsequent, we house out our “sections” utilizing margin-top — i.e. very massive house above every heading
Then we override these massive margin-tops when a heading is adopted instantly by one other heading utilizing the adjoining sibling selector (e.g. h2 + h3).
Now, I don’t find out about you, however I’ve all the time felt it’s higher to make use of a single margin path when spacing issues out, usually favoring margin-bottom (that’s assuming the CSS hole property isn’t possible, which it’s not on this case). Whether or not this can be a massive deal, and even true, I’ll allow you to determine. However personally, I’d relatively be setting margin-bottom for spacing long-form content material.
Collapsing margins
Due to collapsing margins, this mixture of prime and backside margins isn’t an enormous downside per se. Solely the bigger of two stacked margins will take impact, not the sum of each margins. However… properly… I don’t actually like collapsing margins.
Collapsing margins are but another factor to concentrate on. It is likely to be complicated for junior devs who aren’t up to the mark with that CSS quirk. The spacing will completely change (i.e. cease collapsing) if you happen to had been to vary the wrapper to a flex format with flex-direction: column for example, which is one thing that wouldn’t occur if you happen to set your vertical margins in a single path.
I more-or-less know the way collapsing margins work, and I do know that they’re there by design. I additionally know they’ve made my life simpler now and again. However they’ve additionally made it more durable different occasions. I simply assume they’re kinda bizarre, and I’d usually relatively keep away from counting on them.
The :has() resolution
And right here is my try at fixing these points with :has().
To recap the enhancements this goals to make:
No wrapper class is required.
We’re working with a constant margin path.
Collapsing margins are averted (which can or will not be an enchancment, relying in your stance).
There’s no setting kinds after which instantly overriding them.
Notes and caveats on the :has() resolution
At all times verify browser assist. At time of writing, Firefox solely helps :has() behind an experimental flag.
My resolution doesn’t embrace all attainable typographic components. For example, there’s no <blockquote> in my demo. The selector listing is simple sufficient to increase although.
My resolution additionally doesn’t deal with non-typographic components which may be current in your explicit long-form textual content blocks, e.g. <img>. That’s as a result of for the websites I work on, we are likely to lock down the WYSIWYG as a lot as attainable to core textual content nodes, like headings, paragraphs, and lists. The rest — e.g. quotes, pictures, tables, and many others. — is a separate CMS element block, and people blocks themselves are spaced aside from one another when rendered on a web page. However once more, the selector listing may be prolonged.
I’ve solely included h1 for the sake of completeness. I normally wouldn’t permit a CMS person so as to add an h1 through WYSIWYG, because the web page title could be baked into the web page template someplace relatively than entered within the CMS web page editor.
I’m not catering for a heading adopted instantly by the identical degree heading (h2 + h2). This may imply that the primary heading wouldn’t “personal” any content material, which looks like a misuse of headings (and, appropriate me if I’m flawed, however it may violate WCAG 1.3.1 Information and Relationships). I’m additionally not catering for skipped heading ranges, that are invalid.
I’m on no account knocking the present approaches I discussed. If and after I construct one other Tailwind web site I’ll use the wonderful Typography plugin, no query!
I’m not a designer. I got here up with these spacing values by eyeballing it. You in all probability might (and will) use higher values.
Specificity and venture construction
I used to be going to write down an entire massive factor right here about how the standard technique and the brand new :has() manner of doing it would match into the ITCSS methodology… However now that we now have :the place() (the zero-specificity selector) you possibly can just about select your most popular degree of specificity for any selector now.
That stated, the truth that we’re now not coping with a wrapper — .prose, .rich-text, and many others. — to me makes it really feel like this could dwell within the “components” layer, i.e. earlier than you begin coping with class-level specificity. I’ve used :the place() in my examples to maintain specificity constant. All of the selectors in each of my examples have a specificity rating of 0,0,1 (apart from the bare-bones reset).
Wrapping up
So there you may have it, a bleeding-edge resolution to a really boring downside! This newer method remains to be not what I’d name “easy” CSS — as I stated at the start, it’s a extra advanced subject than it may appear at first. However other than having a number of barely advanced selectors, I believe the brand new method makes extra sense total, and the much less inflexible HTML construction appears very interesting.
If you find yourself utilizing this, or one thing prefer it, I’d like to know the way it works out for you. And if you happen to can consider methods to enhance it, I’d love to listen to these too!
Solved With :has(): Vertical Spacing in Lengthy-Type Textual content initially revealed on CSS-Methods, which is a part of the DigitalOcean household. It’s best to get the publication.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!