Skip directly to content

In Between the Lines: The adjacent sibling selector technique

on August 19th, 2010 at 3:18:54 PM

The simple list is a staple of information presentation on the web. Commonly, items in a list are visually distinguished with separating lines - either horizontal borders or vertical borders. This post describes a simple technique for defining styles in the spaces between list items without reference to the first or last item in the list.

Let me illustrate a simple horizontal list and a simple vertical list of items.

Vertical - verbose method

  • Gummy Bears
  • Dino Riders
  • Voltron
  • Thundercats
  • David the Gnome

Horizontal - verbose method

  • Goo Goo Dolls
  • Better Than Ezra
  • Veruca Salt
  • Live
  • Pearl Jam

The lists above are generated with what I'll call the verbose method. This method defines forward facing styles. What I mean by this is that each item pushes the next item down or to the right and provides the style for the border between them. The problem with this method is that the last item pushes nothing but dead space. Therefore, using this method requires specifying the last item in a list with a class, usually .last and then bleaching it of style. Here's the HTML for the example above. Note the .last class on the last item.

<p><strong>Vertical - verbose method</strong></p>
<ul class="verbose vertical">
  <li>Gummy Bears</li>
  <li>Dino Riders</li>
  <li>Voltron</li>
  <li>Thundercats</li>
  <li class="last">David the Gnome</li>
</ul>
<p><strong>Horizontal - verbose method</strong></p>
<ul class="verbose horizontal">
  <li>Goo Goo Dolls</li>
  <li>Better Than Ezra</li>
  <li>Veruca Salt</li>
  <li>Live</li>
  <li class="last">Pearl Jam</li>
</ul> 

The .last class on the last item has always bothered me. It requires extra processing to find this element and mark it - either performed on the server with PHP (for example) or the client through javascript. Here is the CSS for producing this style.

ul {
  list-style: none outside none;
  margin-top: 0;   padding-left: 0;
} 
.horizontal {
  height: 1.75em;
}
.horizontal li {
  float: left;
}
li {
  border-color: #434343;
  border-style: solid;
}
.verbose.horizontal li {
  border-width: 0 1px 0 0;
  margin-right: 0.5em;
  padding-right: 0.5em;
}
.verbose.vertical li {
  border-width: 0 0 1px 0;
  margin-bottom: 0.25em;
  padding-bottom: 0.25em;
}
.verbose.horizontal .last {
  border-right: medium none;
  margin-right: 0;
  padding-right: 0;
}
.verbose.vertical .last {
  border-bottom: medium none;
  margin-bottom: 0;
  padding-bottom: 0;
} 

Now, if you have the luxury of ignoring IE6, there is a better way to produce the same style with less CSS and no need to mark the last item in a list. I call this the adjacent sibling selector technique. The adjacent sibling selector + designates an element that is immediately preceded by another element. For example, if you want to target all <input> tags that follow <label> tags, you could use the following selector.

 label + input {} 

This selector will match the following <input>.

<form>
  <label for="username" />
  <input name="username" value="" />
</form> 

This selector will not match either of the <input>s in the following HTML.

<form>
  <input name="username" value="" /> 
  <input name="password" value="" />
</form> 

So, given this behavior, we can use this selector to flip the style declarations on list elements, creating a reverse facing style approach. Instead of each item pushing the following item down or to the right, each item will push itself away from the previous item. To me, this is a fundamental distinction in CSS. When an item pushes itself around, it isn't necessary for that item to know anything about the semantics of its context i.e. that it's the last item or the first item in a set. But what about the first item you ask? Won't it be pushing itself away from nothing and therefore need to have its style bleached, similar to the last item in the list example above. Simply - no. The first item has no preceding sibling, so the sibling selector will not match the first item in a list. Here's the CSS for the adjacent sibling selector.

ul {
  list-style: none outside none;
  margin-top: 0;
  padding-left: 0;
}
.horizontal {
  height: 1.75em;
}
.horizontal li {
  float: left;
}
li {
  border-color: #434343;
  border-style: solid;
}
.simple.horizontal li + li {
  border-width: 0 0 0 1px;
  margin-left: 0.5em;
  padding-left: 0.5em;
}
.simple.vertical li + li {
  border-width: 1px 0 0 0;
  margin-top: 0.25em;
  padding-top: 0.25em;
} 

These are the important declarations that differ from the verbose example above.

.simple.horizontal li + li {
  border-width: 0 0 0 1px;
  margin-left: 0.5em;
  padding-left: 0.5em;
}
.simple.vertical li + li {
  border-width: 1px 0 0 0;
  margin-top: 0.25em;
  padding-top: 0.25em;
} 

The selector picks out all <li> elements that are preceded by an <li> element. For the horizontal case, the margin/padding are now declared on the left, pushing each item away from its preceding item. In the vertical case, the margin/padding is similarly assigned to the top of the item. In this way, we're targeting the space between items only. We apply it to this HTML. Notice that I haven't designated the first or last item with a class.

<p><strong>Vertical - simple method</strong></p>
<ul class="simple vertical">
  <li>Gummy Bears</li>
  <li>Dino Riders</li>
  <li>Voltron</li>
  <li>Thundercats</li>
  <li>David the Gnome</li>
</ul>
<p><strong>Horizontal - simple method</strong></p>
<ul class="simple horizontal">
  <li>Goo Goo Dolls</li>
  <li>Better Than Ezra</li>
  <li>Veruca Salt</li>
  <li>Live</li>
  <li>Pearl Jam</li>
</ul> 

Here's the rendered output.

Vertical - simple method

  • Gummy Bears
  • Dino Riders
  • Voltron
  • Thundercats
  • avid the Gnome

Horizontal - simple method

  • Goo Goo Dolls
  • Better Than Ezra
  • Veruca Salt
  • Live
  • Pearl Jam

The adjacent sibling selector and its relative the general sibling selector, are very powerful CSS tools. Once you get the hang of using them, you can dream up all kinds of nifty selectors, like this.

.article .block + .block h3 {
  /* h3s in all blocks that follow a block */
}
ul + ul li + li {
  /* list items that follow a list item in an unordered list that follows an unordered list */
}
form label + * {
  /* anything that follows a label inside a form */
} 

So let's drop .last and .first classes for good (once you drop IE6 from your Grade A browser list) and eliminate CSS declarations that exist only to undo styling.

 

Post new comment