CSS Generated Content Techniques


The content property was introduced in CSS 2.1 to add generated content to the :before and :after pseudo-elements. This is now supported on all major browsers — Firefox 1.5+, Safari 3.5+, IE 8+, Opera 9.2+, Chrome 0.2+). In addition, Opera 9.5+ supports the content property on all elements, not just the :before and :after pseudo-elements.

In the CSS 3 Generated Content Working draft, the content property has had a lot more features added – for example, the ability to insert and move content around a document to create footnotes, endnotes, and section notes. But no browser has implemented the expanded functions of content yet.

In this article, we will look at the basics of using generated content, and then break out into specific techniques you can employ it in.

A few caveats

Before we dive into the subject, it's worth pointing out that generated content

  • will only work in modern browsers with CSS enabled
  • is not available via the DOM. It is meant to be purely presentational. In particular, from an accessibility point of view, current screen readers don't support generated content.

Generated content — the basics

content is used like this:

h2:before {
	content: "some text";

This will insert "some text" just before the start of every h2 element on the page.

Instead of typing in the text value for the content property, you can also use values of attributes using attr(), like this:

a:after {
	content: attr(href)

This will insert the contents of each a element's href after the end of the elements.

Note that you need to use the attribute name without quotes when referring to it in attr().

You can also generate dynamic numbers using counter or insert images using url(/path/to/file). Let's go through some applied examples.

Numbering content with counters

If you want to insert an incremental value such as Question 1, Question 2, Question 3 on a repeated sequence of elements, you can use counters to increment the counter value and then display the count appropriately using content:

ol {
	counter-reset: sectioncounter;

li:before {
	content: "Chapter" counter(sectioncounter);
	counter-increment: sectioncounter;

In the first rule, the counter is reset to 1 using the counter-reset property. In the second rule each li element has a string printed out before it of Chapter X, where X is the current value of the sectioncounter counter. The last property in the second rule — counter-increment — increases the value of the sectioncounter counter by 1 before moving on to the next li element in the list.

If you are inserting counters with content, be mindful that they will not be incremented if that element has the display:none specified on it.

Of course, in browsers that don't support this CSS feature, no numbering will appear. This would then make it confusing if, somewhere in your page, you refer back to sections with See Chapter X for more details. This is the fine line between generated content being purely decorative or actual part of the content, which should be written in the actual HTML.

I have written a demo to illustrate the creation of counters with generated content. For more detail on the subject, read David Storey's excellent article on Automatic numbering with CSS Counters.

Inserting correct quotes for multi-language content

Different languages use different characters for quotation marks. A quote in English would be written as:

“It’s only work if somebody makes you do it”

A quote in Norwegian is written in this manner:

«Hvis du forteller meg nok en vits, så skal jeg slå deg til jorden.»

Instead of using simple text with hardcoded quote marks in your HTML, you can use the q element

<p lang="en"><q>It’s only work if somebody makes you do it</q></p>
<p lang="no"><q>Hvis du forteller meg nok en vits, så skal jeg slå deg til jorden.</q></p>

and specify the quote marks appropriate for each language in your your CSS

/* Specify quotes for two languages */
:lang(en) > q { quotes: '“' '”' }
:lang(no) > q { quotes: "«" "»"}

/* Insert quotes before and after <q> element content */
q:before { content: open-quote }
q:after  { content: close-quote }

You can use this technique on any element, not just q (though this is the most obvious and semantic use). Be aware that Safari 3 (and below) and IE 7 (and below), do not support quotes property.

View my Quote-inserting demo to see this in action.

Replacing text with images

There are several image replacement techniques that you could use, each with their own merits and flaws. Here is another way to replace text with images, using content.

div.logo {

The advantage of using this technique for image replacement is that it truly replaces the text. You therefore do not have to resort to using height and width to create space for the image, and text-indent or padding to hide the original text.

However, there are some drawbacks:

  • you cannot repeat the image, or use an image sprite
  • it will work only on Opera 9.5+, Safari 4+, Chrome which support the content property with url as value on all selectors, not just :before or :after
  • there is no way to include alternative text using this method, so screen readers in particular won't be able to make sense of your content-replaced images.

To learn more, check out my demo of Image replacement using content.

Displaying link icons

You can use attribute selectors with the content property to render icons after a link based on what file format it is or if it is an external one.

a[href $='.pdf']:after {

a[rel="external"]:after {  /* You can also use a[href ^="http"]:after */

the first rule uses a CSS3 selector with substring matching — href $='.pdf' means href attributes with .pdf at the end of the value.

As with regular expressions, ^ and $ refer to the start and end of a string, respectively. With CSS 3 substring matching attribute selectors, [attribute^=value] and [attribute$=value] allow you to match elements whose attribute content starts or ends with the specified value, while [attribute*=value] selects elements where the value is found anywhere in the attribute.

Here is a demo displaying PDF and external icons on links.

Using attribute values as content

We already mentioned that content: attr(val) allows you to display the value of an element's attribute on the screen. This can be used in a number of useful ways — here are a couple of examples.

Printing URLs/abbreviations in print CSS

As mentioned in the article Going to Print on A List Apart, you can use generated content to spice up your pages once they're printed out. For instance, the following in your print CSS to print the URL of a link just after it:

a:after {
	content: "(" attr(href) ")";

You can use the same method to print the expansion of your abbr elements. Simply add the following to your print stylesheet:

abbr:after {
	content: "(" attr(title) ")";

View my demo of printing URLs and abbreviation expansions to learn more.

Looking ahead: powerful attr() in CSS3

The CSS3 Values and Units draft expands the scope of the attr() expression — in addition to returning strings, it can also return values such as the unit type of CSS colors, CSS integer, length, angle, time, frequency, and other units.

Along with custom data attributes, this could be really powerful for rendering simple charts, graphics and animation. For example, we could set the background color of an element based on its attribute value. This might be useful in applications that display color palettes on the web. We could also specify the size of an element based on the value set in a custom data attribute – for instance, the length of a bar in a bar chart could be set by an attribute of the element representing the bar. Unfortunately this feature is low priority and is not close to being finalised anytime soon.


Hopefully this article has given you a better understanding of the content property, and what you can use it for. Given that IE 8 also now supports content, we really are getting to the point where can use this CSS feature in our production work. Just be sure to use it only in appropriate circumstances, and be mindful of the accessibility implications that generated content still has.