Tip of the week #012: How to style CMS-generated rich text

Styling is easy when you control the markup. You can add classes to your liking, and style them accordingly. Dealing with CMS-generated HTML however is another thing, having no control over the markup and attributes.

This tip is an extension of the "Lobotomized Owl Selector", created and coined by Heydon Pickering. It's utilizing the next sibling selector (+), so if you haven't used it before you can read more about it on MDN.


HTML generated by a rich text editor

Most CMS'es has a rich text editor, like Portable Text in Sanity or TinyMCE in Wordpress.

The Portable Text Editor in Sanity
The Portable Text Editor in Sanity

Here you can format text as headings, paragraphs, bullet lists, anchors (links) and blockquotes. Your input generates HTML tags. Headings becomes <h1>, <h2> etc., paragraphs becomes <p>, bullet lists becomes <ul> and so on.

The code you recieve may look like this:

Language: html
<h1>The main heading</h1>
<p>This is a paragraph introducing the content of the document. It provides a brief overview of the topics covered and offers insight into the subject matter.</p>

<h2>Subheading One</h2>

<p>This section delves deeper into the first main point. The text can cover details, facts, or anecdotes relevant to the subject. Here’s an example of a blockquote:</p>

<blockquote>
    <p>“This is a sample blockquote. Blockquotes are used to emphasize a section of text, often quotations or important points.”</p>
</blockquote>

<h3>Sub-subheading under Subheading One</h3>

<p>Additional details can be provided in subsections. For instance, you can organize content <em>hierarchically</em> for better <strong>readability</strong>. Readability is important for <code>code</code> as well.</p>

<h2>Subheading Two</h2>

<p>Here is another section with some more content. Let's list out a few key points below:</p>

<ul>
    <li>First key point in an unordered list.</li>
    <li>Second key point that further explains the idea.
        <ul>
            <li>First key point in an unordered list.</li>
            <li>Second key point that further explains the idea.</li>
            <li>Third key point with additional thoughts.</li>
        </ul>
    </li>
    <li>Third key point with additional thoughts.</li>
</ul>

<p>You can also use ordered lists when sequence matters:</p>

<ol>
    <li>First step in a process.</li>
    <li>Second step to follow.</li>
    <li>Third and final step.</li>
</ol>

<h2>Conclusion</h2>

<p>Summarizing everything discussed, the conclusion wraps up the main points and <a href="https://mdn.com">leaves the reader with a final thought or call to action.</a></p>

Now, let's style it.

Style the markup

As you can see, there's no classes to which you can apply styling. If I want to style the headings for instance, I can only target the HTML selectors, like this: h2 { font-size: 1.5rem; font-weight: 700 }.

This is not ideal, because the selector will hit every <h2> on the website. That's not something I want.

The solution is to wrap the generated HTML in a container, so that you'll get a parent selector to latch on to:

Language: html
<div class="rich-text">
  <!-- The rest of the code -->
</div>
Language: css
/* A more specific selector that only targets the direct children of .rich-text */
.rich-text > h2 {
  font-size: 1.5rem;
  font-weight: 700;
}

So far so good. I can continue to add styling for all the elements that's being generated by the editor.

Language: css
.rich-text > :where(h1, h2, h3, h4, h5, h6) {
  font-weight: 700;
}

.rich-text {
  font-size: 1.125rem;
}

.rich-text > h1 {
  font-size: 2.25rem;
  line-height: 1.25;
}

.rich-text > h2 {
  font-size: 1.75rem;
  line-height: 1.25;
}

.rich-text > h3 {
  font-size: 1.5rem;
  line-height: 1.375;
}

.rich-text > h4 {
  font-size: 1.125rem;
}

.rich-text > h5 {
  font-size: 1rem;
}

.rich-text > h6 {
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.5em;
}

.rich-text > blockquote {
  font-size: 1.375rem;
  padding-block: 0.5em;
  padding-inline-start: 0.75em;
  border-inline-start: 0.25rem solid lightgray;
}

.rich-text > p > a:not([class]) {
  font-weight: 700;
  text-decoration: underline;
  color: blue;
}

Add spacing

We can add a nice spacing between the elemnts by utilizing the "Lobotomized Owl Selector".

Language: css
.rich-text > * {
  margin: 0;
}

.rich-text > * + * {
  margin-block-start: 1em;
}

Let's break it down:

  • .rich-text > * targets every direct children of the .rich-text wrapper
  • * + * targets only the elements that follow other elements
  • margin-block-start: 1em adds margin above the element, based on it's font-size, making a nice, relative spacing

See the Pen Untitled by Håvard Brynjulfsen (@havardob) on CodePen.

Headings have a greater font-size than paragraphs, so the margin above them will also be greater. This is a nice one-size-fits-all solution, but I prefer to micro-manage it a bit:

Language: css
/* Removes all margins from the elements */
.rich-text > * {
  margin: 0;
}

/* Adds a generic default margin */
.rich-text > * + * {
  margin-block-start: 1em;
}

/* Adds even greater margin to headings */
.rich-text > * + :where(h2, h3, h4, h5, h6) {
  margin-block-start: 1.5em;
}

/* Adds smaller margin to all elements that follows headings */
.rich-text > :where(h2, h3, h4, h5, h6) + * {
  margin-block-start: 0.5em;
}

/* Adds spacing to list items within a list, and lists within lists */
.rich-text > :where(ul,ol) li + li,
.rich-text > :where(ul, ol) :where(ul, ol){
  margin-block-start: 1em;
}

This method can be extended to every element the rich text editor spews out, like blockquote or table. You can also decide what margins based on what follows what. A table + a list can have a greater margin than a list + a table, etc.

Language: css
.rich-text > table + ul {
  margin-block-start: 2em;
}

.rich-text > ul + table {
  margin-block-start: 1.5em;
}

This is totally up to you, so create as many rules as you need.

A note on markers

Most rich text editors give you the ability to select text and add some form of marks, in the form of bold text, emphasis (italic), code or even mark a line of text as a link.

Markers in my Sanity editor. Note the symbols for bold, italic and code in the toolbar.
Markers in my Sanity editor. Note the symbols for bold, italic and code in the toolbar.

A rich text editor will generate markup like so:

Language: html
<p>Most rich text editors give you the ability to select text and add some form of marks, in the form of <strong>bold</strong> text, <em>emphasis</em> (italic), <code>code</code> or even <a href="link-to-destination">mark a line of text as a link.</a></p>

We have to style these markers as well. strong and em probably already have a nice default style built into the browser (unless you've overwritten it), but we'll want to at least style anchors (a) and code.

It's tempting to write .rich-text a { color: blue } to style links, but that can give us unwanted concequences.

You see, most rich text editors also gives us the ability to add blocks or components, and we have to be careful not to meddle with the styles of these.

Here's an example of rich text content generated in Optimizely when a custom card component is added as a block:

Language: html
<div class="rich-text">
  <p>A paragraph written in the editor</p>
  <div class="block">
    <div class="card">
      <figure class="card__image">
        <img src="/path-to-img" alt="" />
      </figure>
      <h2 class="card__title">
        <a href="#" class="card__link">Card title</a>
      </h2>
      <p class="card__text">An intriguing offer</p>
    </div>
  </div>
  <p>A paragraph written in the editor with <a href="#">an inline link</a></p>
</div>

Targeting .rich-text a { color: blue } is too intrusive and will affect the card component's link. We need to be more specific:

Language: css
/* Links */
.rich-text > :where(p, blockquote, blockquote p) > a:not([class]),
.rich-text > :where(ul, ol) li a:not([class]) {
  color: blue;
}

/* Code */
.rich-text > :where(p, blockquote, blockquote p) > code,
.rich-text > :where(ul, ol) li code {
	background-color: lightgray;
	display: inline-block;
	padding: 0.25em 0.5em;
	border-radius: 0.25em;
	font-size: 0.875rem;
	font-family: monospace;
}

Let's break down the link styling. Here I target all anchors that do not have a class (a:not([class])) within

  • paragraphs (p)
  • blockquotes (blockquote)
  • paragraphs within blockquotes (blockquote p)
  • list items (li)

Inline links may appear other places, but this is a good place to start. This code will not affect the contents of the block.


In conclusion

Dealing with CMS generated markup can be tricky, but if you are specific enough in your selectors you'll be able to write code that only affects the elements in a rich text field you have no power over. It's tempting to write h2 { font-size: 1.75rem } and target all <h2>s globally, but this will create more headache than necessary. Targeting .rich-text > h2 instead is a more secure way to style rich text content.

The complete code

See the Pen rich-text-spacing complete by Håvard Brynjulfsen (@havardob) on CodePen.