CSS :has() selector and line spacing in long texts

If you have worked with sites containing a lot of long textsespecially with CMS sites where users work in a WYSIWYG editor, then you probably wrote CSS to control the line spacing between various typographic elements – headings, paragraphs, lists, etc.

It is surprisingly difficult to write such styles. That’s why tools like Stack Overflow’s Tailwind Typography plugin and Prose have appeared, although they work far beyond line spacing.

At the time of this writing, Firefox supports :has() when turned on флага layout.css.has-selector.enabled V about:config.

Why is line spacing between typography difficult to work with?

It would seem that it is enough to indicate that each element has – p, h2, ul etc. – there must be a certain amount of upper and / or lower margin (margin), right? Unfortunately, not everything is so simple. Let’s assume that you need to fulfill the following requirements:

  • There should be no extra space above the first and below the last element in a block of long text so that the non-typographic elements around the text are positioned correctly.
  • There should be plenty of spacing between sections in long text. The “section” here is the heading and all the text that goes with it: a lot of spacing is needed before the heading, but not thenwhen there is another one right before the heading!

After h3 is a paragraph, and after h2 is another h3.

If the h3 is preceded by a typographic element, such as a paragraph, more spacing is needed, and if it is preceded by another heading, then the spacing should be smaller.

Let’s see where it comes in handy. A couple of screenshots at intervals from another article:

h2 directly above h3.

h3 immediately after the paragraph and line spacing.

Traditional Solution

As a rule, this problem is solved by wrapping long content in div (or into a semantic tag if needed). I usually call the wrapper .rich-text is a legacy of older versions of Wagtail CMS that added this class automatically when rendering WYSIWYG content. tailwind typography uses класс .prose (and some other modifier classes).

CSS is then added to select all the typography elements in that wrapper, and vertical margins. Of course, this takes into account the above requirements for consecutive headings and for the first and last elements.

The traditional solution looks logical… but what’s the problem? It seems to me that there are several problems here.

Rigid structure

The need to add a wrapper class like .rich-text means embedding a special structure in the HTML code. In this case, this is not required. It’s easy to forget to add such a class in the right place, especially if you’re using a mix of CMS and content that won’t change.

The HTML structure loses flexibility if the top and bottom margins of the first and last elements should be removed, since they must be direct children of the wrapper element, for example, the selector is important .rich-text > *:first-child. >because with its help we will randomly select the first element in each ul or ol.

Using Different Sides of Margins

Before the advent :has() there was no way to select an element depending on the next element. This means that the traditional approach to creating line spacing for typographic elements is to use and margin-topAnd margin-bottom:

  1. First with margin-bottom define the default line spacing.
  2. Then, using an adjacent selector (for example, h2 + h3), create a line spacing for the “sections” through margin-top — for example, large spacing before each heading.
  3. We overwrite these large intervals in the case when another heading immediately follows.

I don’t know about you, but I’ve always felt that it’s better to use only one side of the indent when defining line spacing, usually margin-bottom (assuming that in this case CSS-свойство gap not applicable. Whether it’s worth it, I leave it up to you. But for setting line spacing of long content I prefer margin-bottom.

Collapsing margins

Because of margin collapse simultaneous application margin-bottom And margin-top in itself not a problem. Of the two outer paddings stacked on top of each other, only the larger one will be visible, not the sum of the padding values. But I don’t like collapsing margins. They are also worth taking into account.

Collapsing indents can be confusing for beginners who don’t know about this feature of CSS. Line spacing will change (for example, stop collapsing) if you give the wrapper, say, the property flex With flex-direction: column. This will not happen if only one side of the padding is used when defining vertical margins.

I more or less understand how margin collapse works, and I realize that this is done on purpose. Sometimes it helps, but sometimes it doesn’t. Collapsing seems weird to me and I generally try to avoid it.

Solution via :has()

Let me remind you what I want to achieve:

  • Get rid of the wrapper class.
  • Use only one side of the indent.
  • Avoid collapsing margins (you may or may not consider this an improvement).
  • Get rid of setting styles and overwriting them immediately.

This is how I tried to solve these problems via :has().

Notes and explanations for the solution using :has()

  • Always check if the browser supports :has(). At the time of this writing Firefox поддерживает :has() only after setting the experimental flag.
  • My solution does not take into account all existing typography elements. For example, in my demo there is no support <blockquote>. But the list of selectors is easy to expand.
  • My solution doesn’t work with non-typographic elementswhich can be present in certain blocks with long text, for example, <img>. The fact is that I work with sites where WYSIWYG editors are as limited as possible to basic text elements such as headings, paragraphs and lists. Other elements (quotes, pictures, tables, etc.) are in a separate CMS component block. These blocks are spaced apart when rendered on the page. But, I repeat, the list of selectors is easily supplemented.
  • I turned on h1 just for the sake of completeness. I usually don’t allow the use h1 in a WYSIWYG editor: so the page title is somewhere in the layout, and you need to change it using the CMS page editor.
  • I did not foresee the situation when one heading is immediately followed by another of the same level (h2 + h2). This would mean that the first header has no content, which looks like a misapplication of headers (and correct me if I’m wrong, but this Maybe violate the principles WCAG 1.3.1 Info and Relationships Web Content Accessibility Guidelines)). I also didn’t provide skipping heading levels.
  • I am in no way detracting from the merits of the approaches I have previously mentioned.. When I build a website with Tailwind, of course I use the awesome Typography plugin.
  • I am not a designer. I set line spacing by eye. You should use more appropriate values.

Specificity and structure of the project

Here I was going to write a large section on matching the traditional approach and the new solution with :has() [ITCSSmethodology…Butnowthereis[методологииITCSS…Нотеперьесть:where() (a selector with zero specificity), so that you can choose an appropriate specificity value for any selector.

However, a wrapper like .prose, .rich-text etc. is no longer used. This makes me think that they are needed at the “element” level, that is, before you get into specificity at the class level. In my examples, to maintain specificity, I used :where(). All selectors in my examples have specificity valuesequal to 0,0,1 (except for a simple reset).


You have an ultra-modern solution to a very boring problem! I would not call this new approach “simple”. As I said at the beginning of the article, but this task is more difficult than it might seem at first glance. But besides having a few simple selectors, I find this approach much more logical, and the less rigid HTML structure is very attractive.

If you use my solution or something similar, please tell me about the results. And if you find a way to make it better, share your discoveries with me!

Brief catalog of courses

Data Science and Machine Learning

Python, web development

Mobile development

Java and C#

From basics to depth


Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *