zachleat’s Ugly Mug

Zach Leatherman

The Elusive :target Feature Test

20 November 2013Read this in about 4 minutes.

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) {
  e.preventDefault();
  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) {
  e.preventDefault();
  // 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.)

Demos

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

Related

Say hello on and GitHub.

Let my Feed sit idle in your RSS Reader.

Zach Leatherman is a Professional Front End Engineer. He loves building for the web and has been writing here since 2007.

He enjoys spending time with his beautiful wife Traci and their two Great Danes, Roxie and Ella. They also have a cat, a rabbit, goldfish, and usually one or more tarantulas. Read more »