Better CSS-only tabs, with
CSS-only tabs are a fun topic, and
:target is a delightfully elegant declarative approach, except for the bit where it doesn’t work. The #hash links involved jump you around the page by default, and disabling that jumping breaks
:target in your CSS, due to poorly defined behaviour in the spec, and corresponding bugs in every browser.
Or so we thought. Turns out there is a way to make this work almost perfectly, despite these bugs, and get perfect CSS-only accessible linkable history-tracking tabs for free.
What? Let’s back up.
:target is a CSS psuedo-class that matches an element if its id is the hash part of the URL. If you’re on
http://example.com#my-element for example,
div:target would match
<div id="my-element"> and not
This ties perfectly into
<a href="#my-element">. All of a sudden you can add links that apply a style to one part of the page on demand, with just simple CSS.
There’s lots of fun uses for this, but an obvious one is tabs. Click a link, the page updates, and that part of the page is shown. Click another link, that part of the page disappears, and a different part is shown. All with working history and linkability, because the URL is just updating like normal.
Implementation is super easy, and works in 98% of browsers, right back to IE9. It looks like this:
Hop, Skip, and Jump
If you try to do this in any page longer than the height of your browser though, you’ll quickly discover that this can jump around, which can be annoying. Try http://output.jsbin.com/melama — you’ll have to scroll back up after hitting each tab. It’s functionally fine, and in some cases this behaviour might be ok too, but for many it isn’t exactly what you want.
This disables the normal behaviour on every #hash link, and instead manually updates the hash and the browser history. Unfortunately though it doesn’t work. The URL updates, but the
:target selector matching doesn’t!
You end up with
#tab-1 in the URL, but
<div id="tab-1"> doesn’t match
:target. The spec doesn’t actually cover this case and every single browser currently has this crazy behaviour (although both the spec authors and browser vendors are looking at fixing this). Game over.
Two steps forward, one step back
We can beat this. Right now we can disable jumping around, but it breaks
:target. We need a way to disable jumping around, but still update the hash in a way that
:target will listen to.
There’s a few ways we might be able to work around this. Ian Hansson has a clever trick where you
position: fixed and hide the targeted element, to control the scroll, but depend on its
:target status with sibling selectors. Meanwhile Chris Coyier has suggested capturing the scroll position and resetting it, and I think there might be a route through if you change the element’s id to something else and back at just the right time too. These are all very hacky though; it’d be nice to come up with a way of just fixing the JS we want to use above, so it actually works properly.
I’ve found a good reason to believe that virtually no webpages would be broken by the “breaking” change of updating
pushState: though they are currently not updated, if you however hit Back and then Fwd again, then
:targetrules do get applied
Moving in the browser history give us the
:target behaviour we (and all sane people) are expecting. If we can find a way to transparently go back and forward without breaking the user’s experience, we can fix
Here we add the hash to the history twice, and immediately remove it once.
This isn’t perfect, and it would be nice if
:target worked properly without these workarounds. As-is though, this gives perfect behaviour, with the only downside being that the Forward button in the user’s browser isn’t displayed as disabled, as they might expect. Actually going forward won’t do anything though (it’s the same URL they’re already on), and your users are not typically going to notice this.
This will keep working even if/when
This lets you build amazingly simple & effective CSS-only tabs.
Minimal clean markup & CSS, perfect accessibility, working in IE9, with shareable tab URLs and a fully working history for free. Enjoy!
Try it yourself on JSBin:
Useful? Hit the heart to recommend this to the world, and follow me for even more CSS magic (and more) in future. Questions? Twitter at me.