Zach’s ugly mug (his face) Zach Leatherman

The Elusive :target Feature Test

November 20, 2013

Update: I rewrote this post after determining my feature test was unnecessarily complex. Check out the post diff.

Update #2: This has made its way into Modernizr.

If you’re not familiar with :target, check out this very simple example before reading this post.

:target is a CSS pseudo-class that is often overlooked. It’s a very useful tool in the performance toolkit for moving content hiding from JavaScript up the toolchain into CSS. “Anything CSS can do, JavaScript can do worse”.

With proper Progressive Enhancement, using :target can unfairly feel like it’s creating additional work for you to do since you’ll often still have to write that JavaScript for browsers that don’t support :target (IE8 and below don’t support it—but surpringly Android 2.1+ and Windows Phone 7.5 do). The benefit of using :target is perceived performance. The non-primary content (non-active pages/tabs/menus/et cetera) is hidden higher in the waterfall (assuming a best practice of loading your CSS at the top of the page) than they would’ve been if the hiding code lived in JavaScript.

Another great benefit to using target is that it allows you to use history entries when navigating around on the page (and direct linking to specific content) unless you opt-out of this feature using JavaScript. Chris Coyier describes this is in his post On Target (see the section titled ‘Fighting the Jump’). One limitation of this approach is the current WebKit (and Blink) and Gecko bugs that do not reevaluate CSS when the hash is updated using replaceState (without modifying history) or pushState (to add a history entry) (JSBin Example).

$("a[href^=#]").on("click", function(e) {
  history.pushState({}, "", this.href);

There are two issues with this method of Fighting the Jump:

  • The page jumps to the newly targetted content.
  • The CSS rules are not re-evaluated.

We can eliminate the second issue with the long forgotten location.replace() method (or it’s twin brother location.hash). You can use replace in lieu of replaceState (or hash instead of pushState) to update the hash and re-evaluate CSS.

// Will re-evaluate CSS.
// Yes, including any newly applicable :target rules.
location.replace( '#myhash' );

Sadly, this method does not resolve the first issue with “Fighting the Jump.” The page will still scroll to the newly targetted content. But it does allow us to avoid adding a history entry. (If you are fine with the history entry, don’t add any JavaScript at all.)

$("a[href^=#]").on("click", function(e) {
  // Basically the same as doing nothing except we bypass a history entry.
  location.replace( '#' + this.href.substr( this.href.lastIndexOf( '#' ) + 1 ) );

However, we won’t use the same mechanism for our feature test. As it turns out, a :target feature test is much simpler.

Feature Test

Requirements for a good :target feature test:

  • Must be able to execute prior to DOMContentLoaded.
  • Must be unobtrusive and not add a history entry.

A previous version of this blog post included a feature test that attempted to set the hash and measure the updated CSS rules to see if :target applied. This turned out to be woefully overcomplicated. We can exploit the fact that querySelector will throw an error if you feed it an unsupported selector (:target, for example). This means we don’t have to modify the hash at all, which is much safer. (Credit to @wilto for showing me this method.)


The new document.querySelectorAll(':target') Method

  • Chrome 31: Passes
  • Firefox 25: Passes
  • IE7, IE8: Fails Correctly (Not supported)
  • IE9, IE10: Passes
  • Safari 7: Passes
  • Android 2.3: Passes
  • Windows Phone 7.5: Passes
  • BB6.1, BB7: Passes
  • BB5: Passes Correctly and does not require an opt-out like the other method.
  • Kindle 3.4: Passes and does not add a history entry like the other method.
  • Opera Mini: Passes correctly (unlike the other method) but requires a server refresh to repaint the page.
  • Opera 9.10: Fails Correctly (Not supported)
  • Opera 12: Passes

The old location.replace Method

(Included for posterity)

  • Chrome 31: Passes
  • Firefox 3.6: Opts out of the test to use document.scripts. Otherwise, would pass.
  • Firefox 25: Passes
  • IE6, IE7, IE8: Fails Correctly (Not supported)
  • IE9, IE10: Passes
  • Safari 4, 5, 5.1, 6, and 7: Passes
  • Opera Mobile: Passes
  • Android 2.3: Passes
  • Android 4.1: Passes
  • Windows Phone 7.5: Passes
  • iOS 7: Passes
  • BB6.1, BB7: Passes
  • BB5: Opt-ed out of the test using a weak inference. This test caused BB5 to go all infinite redirect, so the feature test hard-returns false for any non-WebKit Blackberry.
  • Kindle 3.4: Passes (But adds a history entry)
  • Opera Mini: Fails Incorrectly (Is supported, but buggy)
  • Opera 9.10: Fails Correctly (Not supported)
  • Opera 12: Passes

< Newer
Domain Swap: A Google Chrome Extension
Older >
Impostor Syndrome

Zach Leatherman IndieWeb Avatar for a builder for the web at IndieWeb Avatar for He is the creator and maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 79 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

  1. Paul Irish Disqus

    21 Nov 2013
    Zach was kind enough to submit a PR for Modernizr, so it'll now have this updated and robust feature test:
  2. Leszek Pietrzak Disqus

    30 Nov 2013
    The :target is indeed quite interesting -- I'm using it for a mobile-style menu at my new portfolio that I'm developing while learning to use github:
Shamelessly plug your related post

These are webmentions via the IndieWeb and

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)