Here’s what I didn’t know about “content”

posted on

This is part 3 of my series Here’s what I didn’t know about… in which I try to learn new things about CSS. This time I'm trying to find out what I didn’t know about the content property.

A few weeks ago Stefan published a post on his website called “The CSS "content" property accepts alternative text”, which blew my mind. He showed that the content property excepts 2 values and not just 1, the actual content and an alternative text.

.new-item::before {
  /* "Highlighted item" and element content is read out */
  content: '★' / 'Highlighted item';
}

I didn’t know that and I was wondering if there were more things I didn’t know about the content property. Since you’re reading this, I found something, so let’s see what I was able to add to my “Here’s what I didn’t know about…” series.

How I’m using the content attribute.

Before I started my research, I was using this property primarily for 3 things.

Adding an element to another element using pseudo elements

If I want to create a simple shape in CSS that is not a rectangle or circle, I use :after and ::before to give myself more options for styling.

<div></div>
div {
  width: 70px;
  height: 50px;
  margin-top: 15px;

  border: 5px solid #123456;
  border-radius: 5px;

  position: relative;
}

div::before {
  content: '';

  position: absolute;
  left: 0;
  right: 0;
  top: -16px;

  width: 20px;
  height: 20px;
  margin: auto;

  border: solid #123456;
  border-width: 5px 5px 0 0;
  border-radius: 5px;

  transform: rotate(-45deg);
  background: #fff;
}

Checkout example 1 on CodePen.

To render on screen, the pseudo elements needs the content attribute.

Revealing URLs in print styles sheets

Printed links are useless if you don’t know where there are leading. I’m using a combination of content and the attr() function in print style sheets to display URLs next to their linked text.

Max Böck

@media print {
  a[href^="http://"]:after,
  a[href^="https://"]:after
  {
    content: ' (' attr(href) ')';
  }
}

Custom counters

Every now and then I need custom counters in lists. A combination of content and counter properties usually does the job.

  1. Element 001
  2. Element 002
  3. Element 003
<ol>
  <li>Element 001</li>
  <li>Element 002</li>
  <li>Element 003</li>
</ol>
ol {
  list-style-type: none;
  counter-reset: mylist;
}

li {
  counter-increment: mylist;
}

li::before {
  content: '🤤 ' counter(mylist) ': ';
}

Checkout example 2 on CodePen.

Have a look at Here’s what I didn’t know about list-style-type for more options to style list items.

Now, let’s see what else content can do for us. Here’s what I’ve learned recently:

Content accepts images and gradients

I knew that content accepts the counter and attr functions, but it never came to my mind that it might allow other functions, as well. Whenever I needed an image in a pseudo element, I would use background-image, although content would've worked, too.

div::before {
  content: url('pin.png');
}

Checkout example 3 on CodePen.

If this works with images, it should work with gradients too, right? Yeah, well, no. Chrome seems to be the only browser that renders pseudo elements with gradient content values.

div::before {
  content: linear-gradient(blue, red);
  height: 50px;
  width: 50px;
  display: block;
  border: 1px solid red;
}

Checkout example 4 on CodePen.

You can define alt text for content values (…in Chrome)

What’s the point of using content when background-image has better support? The reason Stefan wrote his post, content supports alt text.

div::before {
  content: url('pin.png') / 'You are here.';
}

Checkout example 5 on CodePen.

Unfortunately, this only works in Chrome (tested on macOS 10.15.4, Chrome 81 with VoiceOver). Firefox and Safari don't even render the pseudo element because the value is invalid. Too bad.

Even if this worked in most browsers, I wouldn’t recommend adding text content to a CSS file. Others working on the project probably wouldn’t expect text coming from a CSS file, things might get messy on sites with multiple languages, auto-translation may not work, and the content is only accessible if the CSS loads successfully.
Adrian Roselli shares an example of a poor practice in Link Targets and 3.2.5.

You can combine text and images

Nor did I know that you can use the url() function as a value, I also didn’t know that you can combine it with text.

div::before {
  content: url('pin.png') 'You are here.';
}

Checkout example 6 on CodePen.

You can only replace the content of a regular element with an <image>

The content property is meant to be used with pseudo elements. You can’t use it to replace a string in an element with another string. This won’t work:

div {
  content: 'You are here';
}

But you can use it to replace a string with an image. The string is still in the DOM but screen readers announce the filename. (You can do it, but you shouldn’t.)

You are here!
<div>You are here!</div>
div {
  content: url('pin.png');
}

Checkout example 7 on CodePen.

There are quotes and no-quotes

Okay, now this one is really cool. Let’s say we have a quote nested in another quote.

<blockquote>
  My mama always said,
  <q>
    Life was like a box of chocolates. You never know what you’re gonna get </q
  >.
</blockquote>
My mama always said, Life was like a box of chocolates. You never know what you’re gonna get.

What you should see in the above example is that the blockquote has no quotes and q has double quotes.

If we add opening and closing quotes to the blockquote using pseudo elements and the content property, the blockquote now displays double quotes and the q automatically single quotes.

blockquote::before {
  content: open-quote;
}

blockquote::after {
  content: close-quote;
}
My mama always said, Life was like a box of chocolates. You never know what you’re gonna get.

Nice! To top it all off, we can even have a combination of the two variations, blockquote with no quotes and q with single quotes.

blockquote::before {
  content: no-open-quote;
}

blockquote::after {
  content: no-close-quote;
}
My mama always said, Life was like a box of chocolates. You never know what you’re gonna get.

The no-open-quote and no-close-quote keywords don’t insert anything, but increment the quotation depth by one.

Nested quotes in different languages

Just because I was curious, here are some variations of the second example in other languages.

French

<blockquote lang="fr">
  Maman disait toujours,
  <q>
    la vie, c'est comme une boîte de chocolats: on ne sait jamais sur quoi on va
    tomber </q
  >.
</blockquote>
Maman disait toujours, la vie, c'est comme une boîte de chocolats: on ne sait jamais sur quoi on va tomber .

Russian

<blockquote lang="ru">
  Моя мама всегда говорила,
  <q>
    Жизнь как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе
    попадётся </q
  >.
</blockquote>
Моя мама всегда говорила, Жизнь как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе попадётся .

German

<blockquote lang="de">
  Mama hat immer gesagt,
  <q>
    Das Leben ist wie eine Schachtel Pralinen. Man weiß nie, was man kriegt </q
  >.
</blockquote>
Mama hat immer gesagt, Das Leben ist wie eine Schachtel Pralinen. Man weiß nie, was man kriegt .

Spanish

<blockquote lang="es">
  Mi mamá siempre decía,
  <q>
    La vida es como una caja de bombones, nunca sabes lo que vas a conseguir </q
  >.
</blockquote>
Mi mamá siempre decía, La vida es como una caja de bombones, nunca sabes lo que vas a conseguir .

Sorry, if I fucked up any of the translations.

There’s counter() and counters()

I'm really not sure if I was aware of the fact there isn’t just a counter() but also a counters() function. However, the difference is that counters() enables nested custom counters.

  1. Element 001
    1. Element 001
    2. Element 002
    3. Element 003
  2. Element 002
  3. Element 003
<ol>
  <li>
    Element 001
    <ol>
      <li>Element 001</li>
      <li>Element 002</li>
      <li>Element 003</li>
    </ol>
  </li>
  <li>Element 002</li>
  <li>Element 003</li>
</ol>
ol {
  list-style-type: none;
  counter-reset: mylist;
}

li {
  counter-increment: mylist;
}

li::before {
  content: '🤤 ' counters(mylist, '.') ': ';
}

Checkout example 8 on CodePen.

You’ll find more ways of using the content property in Adrian’s article https://adrianroselli.com/2019/12/showing-file-types-in-links.html.

Wow, that was a lot. I didn’t expect to write and learn so much. I hope that you’ve learned as much as I did.

Thanks for reading ❤️ and thanks to Stefan for the inspiration for this post.

Updates

28.05.2020 Added a disclaimer about putting text in CSS files, and a link to an article by Adrian Roselli.