The blog

Build a simple CSS-based image navigation

Often designers won’t budge on allowing you to use text for primary navigation menus. In the past images were embedded directly into the HTML and awful javascript functions were used for displaying multiple states. Nowadays, though, you can use images and text to please designers and search engines by doing a bit of simple math and writing some easy CSS.

View a demo | Download a zip

codeface

We’ll start off with a menu that the designer wants us to use. Since it’s a special font that’s not available to most browsers, we’ll have to use an image. In the past coders would embed images directly in the HTML and use gnarly javascript functions to show multiple states (i.e. mouseover/hover state). Today though we can use regular anchor text, an image, and some simple CSS to create an attractive and search engine friendly menu.

Step 1 – The Image

We’ll be creating three states for the image matrix:

  1. Normal
  2. Hover (mouseover)
  3. Active (click)

nav.png

Notice that we’re only using one image for all three states. This is what’s known as a navigation matrix. Each row in our image matrix corresponds to exactly one state. For this example, the top row is our normal state, the middle our hover state, and the bottom our active state.

Step 2 – The HTML

To keep our menu search engine friendly and accessible to screen readers and text-only browsers, we’ll write up some simple HTML. The menu itself is a plain old ordinary unordered list. I’ve given the unordered list an id of #nav, and each of its list items have a unique id. We’ll need to work with both in the CSS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Demo - Image-based Navigation</title>
<link rel="stylesheet" href="style.css" type="text/css" media="screen" title="no title" charset="utf-8" />
</head>
<body>
<div id="page">

  <ul id="nav">
    <li id="nav_Hom"><a href="#home">Home</a></li>
    <li id="nav_Abo"><a href="#abou">About Us</a></li>
    <li id="nav_Con"><a href="#cont">Contact Us</a></li>
    <li id="nav_Blo"><a href="#blog">Blog</a></li>
  </ul>

</div>
</body>
</html>

Step 3 – Take Some Measurements

Now we’ll go into photoshop to get some measurements. We’ll need only a few key numbers:

  • Width & of one row in the matrix
  • Height & of one row in the matrix
  • Width of each menu item

First set up some guides for the matrix image. Be sure to zoom in closely to ensure that the guides are exactly in the right spots.

Setup a grid of guides in Photoshop

Next, slice up the first row into four parts. Then double-click each slice to open the slice dialogue box. We’ll need to grab the height and the width of the slice. For the first item, Home, the height is 47px and the width is 109px. Jot these numbers down somewhere.

Slice each one and grab their dimensions

Grab the measurements for each of the other three slices and jot them down as well. The measurements for my matrix image are as follows (W x H in pixels):

Item Width (px) Height (px)
Home 109 47
About Us 147 47
Contact Us 161 47
Blog 91 47

Notice that all the heights are the same. Makes sense, huh?

Step 3 – The CSS

Now comes the time to put it all together. We’ve got our image matrix and some required dimensions, so let’s get to the CSS.

First the unordered list itself. Let’s remove margin, padding, and list-style (bullets).

1
2
3
4
5
6
7
8
9
#nav {
    margin: 0;
    padding: 0;
    list-style-type: none;
}
#nav li {
    margin: 0;
    padding: 0;
}

We should have something like this:

Unstyled list

Now since each menu item has a specific width, we’re going to need to use CSS positioning. Let’s set the dimensions of the unordered list to show only one row’s worth of area. Also we’ll set the unordered list to have relative positioning and set the anchors to have absolute positioning. To help out certain browsers, we’ll restrict each list-item a little. Set them to float left. And we’ll add display:block and height:47px to each anchor so that the anchor covers the full height of the matrix cell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#nav {
    width: 508px;
    height: 47px;
    position: relative;
}
#nav li a {
    margin: 0;
    padding: 0;
    float: left;
}
#nav li a {
    position: absolute;
    display: block;
    height: 47px;
}

This will give us something really gross:

Gross, but getting there

Next let’s show the matrix image. Add the background image to both the unordered list and each anchor. This will codevent some browsers from displaying an undesirable flicker effect when each item is moused-over.

1
2
3
#nav, #nav li a {
    background: url(images/nav.png) no-repeat;
}

Still gross, but getting there:

Background image has been added

We’re almost there… Now it’s time to do a bit of simple math. Grab those dimensions you jotted down earlier.

Each anchor within the list will need a fixed width. Also, since we’re dealing with absolute positioning, we’ll need to specify how many pixels from the left each anchor should be located.

By default a browser will apply a background image at coordinates (0,0). We’ll need to override that, and to do so, we’ll need to use background-position and set them to negative y-values.

Let’s start with the first menu item, Home. Home will need a fixed width of 109 pixels. It’s the first one to be displayed, so both its left value and its background position will be set to zero. (The first 0 in background-position is the x-value, the second one is the y-value.)

1
2
3
4
5
li#nav_Hom a {
    width: 109px;
    left:  0;
    background-position: 0 0;
}

Naturally we’ll take on the next item in the menu, About Us. Its width needs to be fixed at 147 pixels. But it also needs to be pushed away from the left side of the menu. The Home item takes up exactly 109 pixels, so we’ll set the anchor’s left value to exactly 109 pixels. In addition, we’ll need to tell the browser to start the background position exactly 109 pixels in from the left, hence the negative value for its x-value.

1
2
3
4
5
li#nav_Abo a {
    width: 147px;
    left:  109px;
    background-position: -109px 0;
}

Notice that the anchor text for About Us is located 109 pixels from the left. The other remaining items are still being floated above and on top of the Home anchor, but we’ll fix that shortly.

About Us is in place

Above, the About Us item need to be 109 pixels from the left. But what about the Contact Us item?

Well its width is fixed, so we can set that to be 161 pixels.

But it’ll need to be moved exactly 256 pixels from the left. How do you get this number? Well, you simply add the widths of all codevious items. This sum will give us the space to the left of the item, so we’ll set that to be its left value. (109 + 147 = 256).

And for the background positioning, set the x-value to be the same as the left value, except make it negative.

1
2
3
4
5
li#nav_Con a {
    width: 161px;
    left: 256px;
    background-position: -256px 0;
}

And finally we’ll handle the last menu item, Blog. Its width is 91 pixels. The sum of all the codevious menu items equals 417 (109 + 147 + 161 = 417). This will give us a left value of 417px and an x-value of -417px.

1
2
3
4
5
li#nav_Blo a {
  width: 91px;
  left: 417px;
  background-position: -417px 0;
}

After these additions, we should have something similar to this. Notice that the anchor text is showing up in its right spot.

Anchors in the right spot

Finally, now that the text is positioned appropriately, we’ll hide the text using text-indent. I’ll apply the hiding to the entire unordered list and set the overflow property to hidden.

1
2
3
4
#nav {
    text-indent: -9999px;
    overflow: hidden;
}

This will leave us with a nice menu showing no text. Notice that as you mouse over each item, the link changes from #home, #abou, #cont, or #blog, which come from the href attribute in the HTML.

Anchor text is now hidden

Step 4 – Multiple States

Finally, we’ll set up the last of the CSS to handle multiple states. The same principles as above apply here, with one difference. Upon mouse over, the second row of the matrix needs to be displayed.

To accomplish this, we simply have to modify the y-coordinate of the background image for each anchor. Keep in mind though, that you’ll have to maintain the same x-coordinate as above.

Let’s start with the first item, Home. We’ll only need to modify background-position. The starting point of the background image must be moved one row up, and since each row is exactly 47 pixels in height, we’ll set the y-coordinate to -47px.

1
2
3
li#nav_Hom a:hover {
    background-position: 0 -47;
}

Now when you mouse over the Home menu item, you should see the second row of the image appear:

Mouseover state for Home is working

Add in the hover states for the other three menu items, but make sure to maintain the x-value of the background-position property.

1
2
3
4
5
6
7
8
9
li#nav_Abo a:hover {
    background-position: -109px -47px;
}
li#nav_Con a:hover {
  background-position: -256px -47px;
}
li#nav_Blo a:hover {
  background-position: -417px -47px;
}

You should be able to see the hover states now.

Hover states for all items

Finally, we’ll implement the active states. This is the state that will appear when the mouse is clicked over one of the menu items.

Do the same thing as when implementing the hover states, except use the pseudo-class a:active and make sure to use a y-value of -94px, which will push the matrix down to display the third and final row of the image.

1
2
3
4
5
6
7
8
9
10
11
12
li#nav_Hom a:hover {
    background-position: 0 -47;
}
li#nav_Abo a:hover {
    background-position: -109px -47px;
}
li#nav_Con a:hover {
  background-position: -256px -47px;
}
li#nav_Blo a:hover {
  background-position: -417px -47px;
}

string

Full CSS

Here’s the full CSS. I like to keep the anchor styles organized in a kinda columnar format. I use TextMate, and using this columnar format really helps me keep these styles organized and easily editable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#nav {
  margin: 0;
  padding: 0;
  list-style: none;
  background: url(images/nav.png) no-repeat;
  width: 508px;
  height: 47px;
  position: relative;
  text-indent: -9999px; overflow: hidden;
}
#nav li {
  margin: 0; padding: 0;
  float: left;
}
#nav li a {
  position: absolute;
  display: block;
  background: url(images/nav.png) no-repeat;
  height: 47px;
}

li#nav_Hom a {width:109px; left:  0  ; background-position:   0   0;}
li#nav_Abo a {width:147px; left:109px; background-position:-109px 0;}
li#nav_Con a {width:161px; left:256px; background-position:-256px 0;}
li#nav_Blo a {width: 91px; left:417px; background-position:-417px 0;}

li#nav_Hom a:hover {background-position:   0   -47px;}
li#nav_Abo a:hover {background-position:-109px -47px;}
li#nav_Con a:hover {background-position:-256px -47px;}
li#nav_Blo a:hover {background-position:-417px -47px;}

li#nav_Hom a:active {background-position:   0   -94px;}
li#nav_Abo a:active {background-position:-109px -94px;}
li#nav_Con a:active {background-position:-256px -94px;}
li#nav_Blo a:active {background-position:-417px -94px;}

Conclusion

Since we’ve used CSS to display the image, we can assure the designer that her font choice is used. Also, if you view the source for the site, you’ll notice that it’s just some regular old HTML text. This is great for search engines, text-only browsers, and users of screen readers.

HTML source is still friendly to search engines, text-only browsers and users of screen readers

That wraps it all up. Feel free to download the sample code and ask questions. Hope this helps ya out!

Leave a Reply