There is a content
property in CSS that’s made to use in tandem with the ::before
and ::after
pseudo elements. It injects content into the element.
Here’s an example:
<div
data-done="✅"
class="email">
[email protected]
</div>
.email::before {
content: attr(data-done) " Email: "; /* This gets inserted before the email address */
}

The property generally takes anything you drop in there. However, there are some invalid values it won’t accept. I heard from someone recently who was confused by this, so I had a little play with it myself and learned a few things.
This works fine:
/* Valid */
::after {
content: "1";
}
…but this does not:
/* Invalid, not a string */
::after {
content: 1;
}
I’m not entirely sure why, but I imagine it’s because 1
is a unit-less number (i.e. 1
vs. 1px
) and not a string. You can’t trick it either! I tried to be clever like this:
/* Invalid, no tricks */
::after {
content: "" 1;
}
You can output numbers from attributes though, as you might suspect:
<div data-price="4">Coffee</div>
/* This "works" */
div::after {
content: " $" attr(data-price);
}
But of course, you’d never use generated content for important information like a price, right?! (Please don’t. It’s not very accessible, nor is the text selectable.)
Even though you can get and display that number, it’s just a string. You can’t really do anything with it.
<div data-price="4" data-sale-modifier="0.9">Coffee</div>
/* Not gonna happen */
div::after {
content: " $"
calc(attr(data-price) * attr(data-sale-modifier));
}
You can’t use numbers, period:
/* Nope */
::after {
content: calc(2 + 2);
}
Heads up! Don’t try concatenating strings like you might in PHP or JavaScript:
/* These will break */
::after {
content: "1" . "2" . "3";
content: "1" + "2" + "3";
/* Use spaces */
content: "1" "2" "3";
/* Or nothing */
content: "1 2 3";
/* The type of quote (single or double) doesn't matter, but content not coming back from attr() does need to be quoted. */
}
There is a thing in the spec for converting attributes into the actual type rather than treating them all like strings…
<wood length="12" />
wood {
width: attr(length em); /* or other values like "number", "px", or "url" */
}
…but I’m fairly sure that isn’t working anywhere yet. Plus, it doesn’t help us with pseudo elements anyway, since strings already work and numbers don’t.
The person who reached out to me over email was specifically confused why they were unable to use calc()
on content
. I’m not sure I can help you do math in this situation, but it’s worth knowing that pseudo elements can be counters, and those counters can do their own limited form of math. For example, here’s a counter that starts at 12 and increments by -2 for each element at that level in the DOM.
See the Pen Backwards Double Countdown by Chris Coyier (@chriscoyier) on CodePen.
The only other thing we haven’t mentioned here is that a pseudo element can be an image. For example:
p:before {
content: url(image.jpg);
}
…but it’s weirdly limited. You can’t even resize the image. ¯\_(ツ)_/¯
Much more common is using an empty string for the value (content: "";
) which can do things like clear floats but also be positioned, sized and have a background of its own.
Coooool… I never knew I can count using pure CSS
I wonder if you could use an element’s height/width properties as a stand-in for at least the font-size I’m using on the image itself in this codepen https://codepen.io/spicedham/pen/pxzYYe similar to how you’ve demoed calc in this article. Sounds like maybe not yet.
This is not accesible right, because I think screen reader is not going to read a pseudo element
Interestingly, generated content can contain not just an image, but also multiple images or mix of the text and images (quick example: http://jsfiddle.net/63xLqbet/).
Also, maybe it’s worth noting that the CSS Generated Content Level 3 spec draft seems to differentiate the single image value for the
content
property from the mix of the text and images: the former is called “content-replacement” and should behave like the regularimg
element (including resizing) while the latter is called “content-list” (and behaves like current pseudo-elements content). Unfortunately, implementing the “content-replacement” for pseudo-elements as currently specified is not compatible with the existing content, but probably we could improve the spec together?The one that has tripped me up is trying to use html entities, or raw font-awesome (high ascii) characters. In both cases you must use unicode character codes.
Well done man! Thanks for sharing your knowledge.
Counters can even help with setting the
content
value to avar()
orcalc()
.The
var()
case:The
calc()
case:The image used for the
content
can be a CSS gradient, for example:Live test for all the above stuff.
Caution though – using CSS gradients still doesn’t work in current Firefox, though I’ve recently received an email notifying me that this old bug has been fixed. I hope that also means
-moz-element()
will also work as acontent
value too, as I included that in my original test case as well.That’s some straight CSS trickery right there.
Some screen readers do announce generated content I believe. However it is worth remembering that not all CSS is for web pages. Generated Content is used in the Paged Media model to insert content such as headers and footers. I demonstrate some of that here https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/
Not a big fan of the content property. I feel it violates the separation of content and presentation. It can also complicate i18n / l10n depending on your approach.
Chris: “… a pseudo element can be an image {in the content-attribute] … but it’s weirdly limited. You can’t even resize the image. ¯_(ツ)_/¯”
In this way it’s true. But if you give a background-image to the pseudo element, then a new world is opening. /_☀__/\
Then you can resize to your heart’s content, you can use sprites, make them responsible, animated and more. With the right calculations (or a good trial & error) the sizing is no problem.
Examples: clba.nl/experiments/styling-images-in-pseudo-elements