3. Images and Pseudo-elements
We’re going to make use of an image
sprite for the icons in our application—that is, multiple images combined
into a single graphic and then selectively cropped in different contexts. It’s a technique
that’s often used in desktop web development, as it means there are fewer images for the
browser to download. It may seem counterintuitive, but it’s actually faster for a browser
to download a single large image rather than several small images. That’s because browsers
can only download a limited number of resources in parallel from any server—so the more
images, the longer the queue to download each one.
The usual process of
implementing sprites is to create a single image that can then be used as a background
image for multiple elements. This background image is then repositioned to show the
correct sprite in the correct place. The problem with this approach is that it requires
either:
large, complex images that have enough padding around each sprite for the space
they appear in (to ensure other parts of the sprite don’t bleed in
accidentally)
unnecessary markup that gives us a hook to crop the sprite tightly in place
We’re going to use a technique that avoids both these pitfalls, instead
creating the additional elements we need using content generated in CSS pseudo-elements. This approach also lets us alter the source of the sprite
using only CSS, which will come in handy down the track.
Before we tackle the
code though, we need an image! The one we’re going to use for our app contains all the
icons we need, with extra copies for each additional state we require (in our case we’re
only using a selected state for the icons that appear in the tab bar). The image we’ve
created is shown in Figure 6, and can be found in the code archive as
images/sprite.png.
We’re going to use the “Stars” tab in our tab bar as the demo for this
technique. The first step is to create the pseudo-element that’s going to contain our
sprite. Pseudo-elements can be used to insert “fake” elements into our page via CSS,
allowing us to apply additional background images without affecting the markup of our
page.
There are two pseudo-elements that we can use for the purpose of adding
extra images, and the syntax for creating them is similar to what’s used for
pseudo-classes like :hover and :visited with which
you’re probably already familiar:
:before, which can be used to insert content before the
selected element
:after, which can be used to insert content after the
selected element
In the example below, these two selectors would place their respective
content before and after each paragraph
tag:
p:before {
content: "I'm in front!"
}
p:after {
content: "Bringin' up the rear!"
}
This
may look a little odd if you’re unfamiliar with the content property in CSS. It lets us generate additional
content—like the text in the example above—using only CSS. Both the above pseudo-elements
behave in much the same way, but we’re going to use :after, because the
icon will appear after the text in our tab bar element. Let’s create our new element with
our sprite image:
listing 11. stylesheets/screen.css (excerpt)
#tab-spots a:after { content: url(../images/sprite.png); }
|
A lesser-known feature of the content property is that
it can be used to generate more than just text. If we give it the URL of an image, as
we’ve done above, it’ll create an <img> element using
that URL as its <src>.
Figure 7 shows what this will look like.
“Wonderful,” you say, “but how are we supposed to crop the image?” And again, the answer is with a little CSS magic. We’re going
to use the clip property to trim the sprite down to just the bit we
want. clip lets us define a rectangle inside the bounds of an element
and then crop the content of that element to the rectangle.
Let’s look at a
simple example so that you can understand the
concept:
.example {
background: #000;
height: 100px;
width: 100px;
}
This
code will make a 100×100px box with a black background. By adding a clip property, we can crop its contents without altering the overall size of
the
element:
.example {
background: #000;
clip: rect(0 50px 50px 0);
height: 100px;
width: 100px;
}
Figure 8 shows a before/after comparison (note the original shape
of the div has been included in gray to illustrate the difference).
The element still thinks it’s 100 pixels wide, but only 50 pixels of its
content is visible. You can see how this property can be used to selectively show sections
of our sprite image. The four values in the parentheses after clip:
rect, in order, are the distance to crop from:
the top edge, measured from the top edge
the right edge, measured from the left edge
the bottom edge, measured from the top edge
the left edge, measured from the left edge
The values are in the same order as you’re probably used to specifying
for the margin or padding shorthand properties:
top, right, bottom, left. The most important point to remember is that the origin for all
crop values is the top-left corner of the element. It’s done this way because the top-left
corner is the only anchor point that won’t change relative to the content in the clipped
element. At first this will seem counterintuitive, but once you get the hang of the
syntax, it does make sense.
For the Spots tab in our tab bar, we want to clip
a rectangle that includes only the unselected state for the map pin icon, which works out
to be:
top: 0 pixels from the top edge
right: 18 pixels from the left edge
bottom: 33 pixels from the top edge
left: 0 pixels from the left edge
So our clip value will be:
listing 12. stylesheets/screen.css (excerpt)
#tab-spots a:after { content: url(../images/sprite.png); clip: rect(0 18px 33px 0px); position: absolute; }
|
This gives us the nicely cropped icon we’re after, as Figure 9 shows, though, note that clip only
works on elements that are absolutely positioned.
All that’s left to do now is to wrangle it into the position we want.
Pseudo-elements can be manipulated in the exact same way as standard elements in the DOM,
so we can position our sprite using regular techniques. In this case, we’re going to use
the 50% position and negative margin trick to center the icon in the middle
tab:
listing 13. stylesheets/screen.css (excerpt)
#tab-spots a:after { content: url(../images/sprite.png); clip: rect(0 18px 33px 0px); left: 50%; margin-left: -10px; top: 1.833em; position: absolute; }
|
Perfect! Figure 10 shows the final appearance of
our icon.
One trick with this technique that’s not immediately obvious is that we need
to offset our pseudo-element by an additional amount equal to the distance we’re cropping
in from the left. This is because clip doesn’t reduce the actual size
of the element it’s applied to—so if you’re clipping a rectangle 40px from the left edge
of an image, the resulting image will still show up 40px from where you’ve placed the
image. To offset this, you’ll need to apply a negative margin to pull the element back and
line up the clipped section of the background image.
For the first icon we’ve
placed above (the “Spots” icon), this turns out to be unnecessary, since that icon is the
first one from the left in our sprite. But if you take a look at the code for the selected
state of our “Spots” tab, you can see the additional value. Remember, we’re using the
class applied to the containing <ul> to note the selected
state:
listing 14. stylesheets/screen.css (excerpt)
.page-spots #tab-spots a:after { clip: rect(0 37px 33px 18px); margin-left: −29px; }
|
Here we’re cropping an area that’s 19px wide, so we’re using a negative left
margin of 18px to wrangle it into the correct position—the last value in the rect function. Because we’re already using a negative margin to center the
image (half the total width, 10px), the total negative margin winds up being 28px.
Whew!
So that’s it! Now we can roll the same implementation out to all the
places we need icons.