The Untapped Power of CSS

Today I noticed something really cool on wikipedia: the "edit" links expand into more options when I hover my mouse over them, kind of like an automatic door when I step close to it.

I was like "oh, cool. I bet the CSS for that is really smart..." so I took a look at the DOM and was horrified to see the gigantic mess in the html.

I'm going to show you how to get the same effect using only CSS with very simple HTML.

This is the effect I'm going for: editedit source

And this is it: http://jsfiddle.net/7T5hc/

When I don't have my mouse over it, the HTML looks something like this on Wikipedia:


	[
	edit
	]
	
	
	
	

All those tags just for two links? When I do put my mouse over it, the whole thing changes and its like woah, what's going on here! The formatting and style of the HTML content is all changed by JavaScript. What if we could reemove all of those spans inside mw-editsection and have none of this style attribute jiggery-pokery?

With a purely CSS approach, the same thing will look like this instead:


	edit
	edit source

... and it won't ever need to change. This is a much more efficient approach because it means the browser can handle all of it in CSS, instead of waiting for the HTML DOM to change, update the window with the changes, then apply CSS, and all of this many times per second whenever the mouse moves on or off specific elements on the page. Also, it still works with JavaScript disabled in the browser, because it doesn't use any JavaScript. Notice I've shortened the URLs as well; they still work in exactly the same way.

Step 1: HTML

We start with just one span containing two anchors (links)

<span class="editlinks"><a href="#edit">edit</a><a href="#source">edit source</a></span>

Notice there is no white-space in this html. We want to tightly control all of the position and spacing with CSS (the HTML on Wikipedia also has no white-space). Also notice there are no square brackets or pipes in the html, just a span with a class containing two links and that's all there is to it. Those extra characters have absolutely no purpose whatsoever, except to make things look pretty. HTML is for content, CSS is for style. This is what the content looks like without any style:

editedit source

At this point we have very, very simple html and only one class. No tag attributes and no javascript is involved. Now let's begin with the styling...

Step 2: Before and After

We add square brackets before and after the span like so:

.editlinks:before { content: "["; }
.editlinks:after  { content: "]"; }

Now we want to hide the second link. In fact, we want to hide all of the links in the span, except for the first one. This will allow us to add more links to the span in the future without worrying about the CSS.

Anchor tags have a default "display" type of "inline". We can hide all of the links by changing this to "none" and then unhide the first one by changing it back to "inline". The anchors are children of the span, so the first link is the first child:

.editlinks a {
	text-decoration: none; /* removes the underline style */
	display: none;
}
.editlinks a:first-child {
	display: inline;
}

A minor note here: the CSS 2.1 pseudo-selector ":first-child" works in all browsers except IE 6 and older. There is a very small number of people still using IE6, but it's because they're forced to, and if you don't want to alienate these users, you should use IE Conditional Comments to override the CSS we are using here. You might think "hold on a minute, if this CSS doesn't work on old browsers on old machines, I should use JavaScript instead because it always works", but you need to remember these are very underpowered machines, so eliminating the processor-intensive nature of HTML/JavaScript animation - in fact, eliminating all animation - is probably a better idea.

If we combine those two bits of CSS, this is what we have so far:

[edit]

Step 3: The "Hover" Effect

One of the first things taught to CSS beginners is "a:hover, a:link, a:visited" but did you know that these pseudo selectors also apply to other tags as well? We can use :hover on our span to change its style, but not only that, we can use this selector to select children of the span instead.

Consider this:

.editlinks a {
	display: none;
}
.editlinks:hover a {
	display: inline;
}

Makes sense? Good. Now let's combine it with the CSS above:

.editlinks a {
	text-decoration: none;
	display: none;
}
.editlinks a:first-child,
.editlinks:hover a {
	display: inline;
}

That means:

  1. the default "text-decoration" for "a" is "underline", so let's remove that
  2. all of the ".editlinks a" are hidden (display:none)
  3. but when the ".editlinks" is not hovered, the first "a" is visible
  4. and when the ".editlinks" is hovered, all of the "a" are visible

This is how we add the pipe character after each anchor in the span:

.editlinks:hover a:after {
	content: " | ";
	color: #000;
}

Notice we also set the color of this content because otherwise it would be blue, because it's imitating the content of the anchor it belongs to.

Now we must remove the last pipe character, so that the pipes only appear between the links. Let's find the last link and change the ":after" content to nothing, but only when the ".editlinks" is ":hover":

.editlinks:hover a:last-child:after {
	content: "";
}

This is what we have now:

editedit source

Step 4: Animation without JavaScript (CSS transitions, dude!)

We're almost done! Now we just want the brackets to move away from the links smoothly when we hover.

When the mouse is over the ".editlinks", the "[" should move a little away from the right and the "]" should move a little away from the left:

.editlinks:before,
.editlinks:after {
	position: relative;
}
.editlinks:hover:before { right: 0.3em; }
.editlinks:hover:after  { left:  0.3em; }

The "em" is a unit of measure roughly equivalent to the width of one "m" character in the current font size. We can use it to approximate the width of a space between words.

Now let's add opacity to the :before and :after content so that it partially fades into the background. We're going to reduce opacity to 0.3 out of 1, so that it's only 30% visible:

.editlinks:hover:before { right: 0.3em; opacity: 0.3; }
.editlinks:hover:after  { left:  0.3em; opacity: 0.3; }

We must also apply this opacity effect to the pipe which separates the links:

.editlinks:hover a:after {
	content: " | ";
	color: #000;
	opacity: 0.3;
}

And now the final stage is to say in CSS how much time it should take to change an attribute, so that when the left, right, or opacity attributes change, it shouldn't be instantaneous:

.editlinks:before,
.editlinks:after {
	position: relative;
	transition: left 0.2s, right 0.2s, opacity 0.2s;
}

I've set it to 0.2s here, so that it takes a 5th of a second to go from this [edit] to this [ edit | edit source ]

Check it out :D

editedit source

Step 5: The End

There is a minor adjustment to make; when the square brackets move away from the links, they get a little close to the surrounding content, so lets add some padding to the left of the span. When we put all the CSS together, we get this:

<style type="text/css">
.editlinks {
	padding-left: 5px;
}
.editlinks:before {
	content: "[";
}
.editlinks:after {
	content: "]";
}
.editlinks:before,
.editlinks:after {
	position: relative;
	transition: left 0.2s, right 0.2s, opacity 0.2s;
}
.editlinks:hover:before { right: 0.3em; opacity: 0.3; }
.editlinks:hover:after  { left:  0.3em; opacity: 0.3; }


.editlinks a {
	text-decoration: none;
	display: none;
}
.editlinks a:first-child,
.editlinks:hover a {
	display: inline;
}

.editlinks:hover a:after {
	content: " | ";
	color: #000;
	opacity: 0.3;
}
.editlinks:hover a:last-child:after {
	content: "";
}
</style>

<!--[if lte IE 6]>
<style type="text/css">
	.editlinks {
		padding-left: 0;
		border-left: 1px solid #CCC;
	}
	.editlinks a {
		display: inline;
		padding-left: 0.3em;
		padding-right: 0.3em;
		border-right: 1px solid #CCC;
	}
</style>
<![endif]-->


<span class="editlinks"><a href="#edit">edit</a><a href="#source">edit source</a></span>

Happy coding! :D