It’s time for the afternoon talks at An Event Apart in Seattle. We’re going to have back-to-back CSS, kicking off with Eric Meyer. His talk is called Generation Style. The blurb says:
Consider, if you will, CSS generated content. We can, and sometimes even do, use it to insert icons before or after pieces of text. Occasionally we even use it add a bit of extra information. And once upon a time, we pressed it into service as a hack to get containers to wrap around their floated children. That’s all fine—but what good is generated content, really? What can we do with it? What are its limitations? And how far can we push content generation in a new landscape full of flexible boxes, grids, and more? Join Eric as he turns a spotlight on generated content and shows how it can be a generator of creativity as well as a powerful, practical tool for everyday use.
Wish me luck, ‘cause I’m going to try to capture the sense of this presentation…
So we had a morning of personas and user journeys. This afternoon: code, baby! Eric is going to dive into a very specific corner of CSS—generated content. For an hour. Let’s do it!
He shows the CSS Generated Content Module Level 3. Eric wants to focus on one bit: the pseudo-elements ::before
and ::after
. What does pseudo-element mean?
You might have used one of these pseudo-elements for blockquotes. Perhaps you’ve put a great big quotation mark in front of them.
blockquote:: after {
content: "“";
font-size: 4em;
opacity: 0.67;
/* placement styles here */
}
Why is Eric using ::after
? Because you can. You can put the ::after
content wherever you want. But if your placement styles fail, this isn’t a good place for the generated content. So don’t do this. Use ::before
.
Another example of using generated content is putting icons beside certain links:
a[href$=".pdf"]::after {
content: url(i/icon.png);
height: 1em;
margin-right: 0.5em;
vertical-align: top;
}
But these icons look yucky. But if you use larger images, they will be shown full size. You only have so much control over what happens in there. I mean, that’s true of all CSS: think of CSS as a series of strong suggestions. But here, we have even less control than we’re used to. Why isn’t the image 1em
tall like I’ve specified in the CSS? Well, the generated content box is 1em
tall but the image is breaking out of this box. How about this:
a[href]::after * {
max-width: 100%;
max-height: 100%
}
This doesn’t work. The image isn’t an element so it can’t be selected for.
The way around it is to use background images instead:
a[href$=".pdf"]::after {
content: '';
height: 1em; width: 1em;
margin-right: 0.5em;
vertical-align: top;
background: center/contain;
background-image: url(i/icon.png);
}
Notice there’s a right margin there. That stretches out the width of the whole link. That’s exactly the same as if there were an actual span
in there:
a[href$=".pdf"] span {
height: 1em; width: 1em;
margin-right: 0.5em;
vertical-align: top;
background: center/contain;
background-image: url(i/icon.png);
}
So why use generated content instead of a span
? So that you don’t have to put extra span
s in your markup.
Generated content is great for things that work great when they’re there, but still work fine if they’re not. It’s progressive enhancement.
You’ve almost certainly used generated content for the clearfix hack.
.clearfix::after {
content: '';
display: table;
clear: both;
}
Ask your parents. It’s when we wanted to make the containing element for a group of floating elements to encompass the height of those elements. Ancient history, right? Well, Eric is showing an example of a certain large media company today. There are a lot of clearfixes in there.
Eric makes the clearfix visible:
.clearfix::after {
content: '';
display: table;
clear: both;
border: 10px solid purple;
}
It looks like a span: a 10 pixel wide box. Now change the display
property:
.clearfix::after {
content: '';
display: block;
clear: both;
border: 10px solid purple;
}
Now it behaves more like a div
than a span
.
The big question here is: who cares?
Let’s say we’re making a site about corduroy pillows (I hear they’re really making headlines).
<header>
<h1>Corduroy pillows</h1>
<p>Lorum ipsum...</p>
</header>
We can add a box under the header
:
header::after {
content: " ";
display: block;
height: 1em;
}
You can do stuff with that extra content, like using a linear gradient:
header::after {
content: " ";
display: block;
height: 1em;
background: linear-gradient(to right, #DDD, #000, #DDD) center / 100% 1px no-repeat;
}
The colour stops are #DDD, #000, and #DDD. You get this nice gradiated line under the header
. You can chain a bunch of of radial gradients together to get some nice effects. You could mix in some background images too. Now you’ve got some on-brand separators. You could use generated content to add some “under construction” separators.
By the way, ever struggled to keep track of the order of backgrounds? Think about how you would order layers in Photoshop.
How about if we could use generated content to make design tools?
div[id]::before {
content: attr(id);
}
Now the generated content is taken from the id
attribute. You can make it look like Firebug:
div[id]::before {
content: '#' attr(id);
font: 0.75rem monospace;
position: absolute;
top: 0;
left: 0;
border: 1px dashed red;
padding: 0 0.25em;
background: #FFD;
}
You can even make the content cover the whole box with bottom
and right
values too:
div[id]::before {
content: '#' attr(id);
font: 0.75rem monospace;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border: 1px dashed red;
padding: 0 0.25em;
background: #FFD8;
}
(And yes, that is a hex value with opacity.)
Let’s make it less code-y:
div[id]::before {
content: attr(id);
font: bold 1.5rem Georgia serif;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border: 1px dashed red;
padding: 0 0.25em;
background: #FFD8;
}
Throw in some text-shadow
. Maybe some radial gradients. We’re at the wireframe stage. Let’s drop in some SVG images to show lines across the boxes.
How about automating design touches?
pre {
padding: 0.75em 1.5em;
background: #EEE;
font: medium Consolas, monospace;
position: relative;
}
Let’s say that applies to:
<pre class="css">
...
</pre>
You can generate labels with that class attribute:
pre::before {
content: attr(class);
display: block;
padding: 0.25em 0 0.15em;
font: bold 1em Noah, sans-serif;
text-align: center;
text-transform: uppercase;
}
Let’s align it to the top of it’s parent with negative margins:
pre::before {
content: attr(class);
display: block;
padding: 0.25em 0 0.15em;
margin: -0.75em -1.5em 1em;
font: bold 1em Noah, sans-serif;
text-align: center;
text-transform: uppercase;
}
Or you can use absolute positioning:
pre::before {
content: attr(class);
display: block;
padding: 0.25em 0 0.15em;
position: absolute;
top: 0;
right: 0;
left: 0;
font: bold 1em Noah, sans-serif;
text-align: center;
text-transform: uppercase;
}
Now let’s change the writing mode:
pre::before {
content: attr(class);
display: block;
padding: 0.25em 0 0.15em;
position: absolute;
top: 0;
right: 0;
bottom: 0;
writing-mode: vertical-rl;
font: bold 1em Noah, sans-serif;
text-align: center;
text-transform: uppercase;
}
Now the text is running down the side, but it’s turned on its side. You can transform it:
pre::before {
content: attr(class);
display: block;
padding: 0.25em 0 0.15em;
position: absolute;
top: 0;
right: 0;
bottom: 0;
writing-mode: vertical-rl;
transform: rotate(180deg);
font: bold 1em Noah, sans-serif;
text-align: center;
text-transform: uppercase;
}
But if you this, be careful. Your left margin is no longer on the left. Everything’s flipped around.
You could also update the generated content according to the value of the class
attribute:
pre.css:: before {
content: '{ CSS }';
}
pre.html::before {
content: '< HTML >';
}
pre.js::before,
pre.javascript::before {
content: '({ JS })();';
}
It’s presentational, so CSS feels like the right place to do this. But you can’t generate markup—just text. Angle brackets will be displayed in their raw form.
But positioning is so old-school. Let’s use CSS grid:
pre {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 0.75em;
}
pre::before {
content: attr(class);
margin: -1em 0;
padding: 0.25em 0.1em 0.25em 0;
writing-mode: vertical-rl;
transform: rotate(180deg);
font: bold 1em Noah, sans-serif;
text-align: center;
text-transform: uppercase;
}
Heck, you could get rid of the negative margins by putting the code content inside a code
element and giving that a margin
of 1em
.
You can see generated content in action on the website of An Event Apart:
li.news::before {
content: attr(data-cat);
background-color: orange;
color: white;
}
The data-cat
attribute (which contains a category value) is displayed in the generated content.
Cool. That’s all stuff we can do now. What about next?
Well, suppose you had to put some legalese on your website. You could generate the numbers of nested sections:
h1 { counter-reset: section; }
h2 { counter-reset: subsection; }
Increment the numbers each time:
h2 { counter-increment: section; }
h3 { counter-increment: subsection; }
And display those values:
h2::before {
content: counter(section) ".";
}
h2::before {
content: counter(section) counter ":" (subsection, upper-roman);
}
Soon you’ll be able to cycle through a list of counter styles of your own creation with a @counter-style
block.
But remember, if you really need that content to be visible for everyone, don’t rely on generated content: put it in your markup. It’s for styles.
So, generated content. It’s pretty cool. You can do some surprising things with it. Maybe ::before
this talk, you didn’t think about generated content much, but ::after
this talk ,you will.