Mathias Bynens

Bulletproof HTML5 <details> fallback using jQuery

Published · tagged with CSS, HTML, JavaScript, jQuery

The HTML5 <details> element is currently not natively supported in any browser natively supported in Chrome 12 and up. This makes it a little hard to guess how exactly this new element will behave, but reading the spec gives us a pretty good idea. What’s clear is the following:

A details element represents a disclosure widget from which the user can obtain additional information or controls.

This information is hidden by default. It can be made visible by adding the boolean open attribute to the <details> element. After that, the user can toggle the visibility by clicking the <summary>.

The summary element represents a summary, caption, or legend for the rest of the contents of the summary element’s parent details element, if any. If there is no child summary element, the user agent should provide its own legend (e.g. “Details”).

Note that while the spec describes what should happen in case the <summary> element is omitted, it’s still invalid to do so.

It appears to me the summary element should be keyboard accessible as well. You should be able to tab through all different interactive elements (such as links, buttons, form elements), and guess what — summary is one of them. After focusing it with the keyboard, it would be nice to just hit Enter or Space to toggle visibility of the <details> element’s contents. The HTML spec doesn’t say anything about this matter, though.

Note that details got included into HTML5 because it’s such a common behavior for web sites and apps — so common it should be possible to do this without the need for additional scripting to make the whole thing work. However, HTML5 is still a work in progress. The <details> element is not even implemented yet. In the meantime, it’s important to provide a fallback/polyfill when using new features that aren’t (fully) supported cross-browser.

CSS, JavaScript, and jQuery to the rescue

Luckily it’s pretty easy to make <details> work cross-browser using a combination of CSS, (plain) JavaScript, and jQuery.

Detecting native <details> support with JavaScript

First, let’s add class="no-details" to the html element if JavaScript is enabled and the browser does not support <details> natively. This will allow us to write CSS specifically for the occasion our scripts will actually be executed. Adding the class can be done in various ways, but the one I prefer is by placing the following snippet in the <head>:

<script>
	// Don’t use this! See note below.
	if (!('open' in document.createElement('details'))) {
 		document.documentElement.className += ' no-details';
	}
</script>

Update: Chrome 10 recognizes the open attribute for <details> elements, even though it doesn’t support rendering the element correctly yet. This means the above feature detection method has become unreliable. I’ve now created a more robust feature test for <details>/<summary> support:

var isDetailsSupported = (function(doc) {
	var el = doc.createElement('details');
	var fake;
	var root;
	var diff;
	if (!('open' in el)) {
		return false;
	}
	root = doc.body || (function() {
		var de = doc.documentElement;
		fake = true;
		return de.insertBefore(doc.createElement('body'), de.firstElementChild || de.firstChild);
	}());
	el.innerHTML = '<summary>a</summary>b';
	el.style.display = 'block';
	root.appendChild(el);
	diff = el.offsetHeight;
	el.open = true;
	diff = diff != el.offsetHeight;
	root.removeChild(el);
	if (fake) {
		root.parentNode.removeChild(root);
	}
	return diff;
}(document));

So after that, we can just do something like:

<script>
	if (!isDetailsSupported) {
		document.documentElement.className += ' no-details';
	}
</script>

Note that Modernizr currently doesn’t detect <details> support. I’m sure it will be included soon. Update: My feature test for <details> is now included in the Modernizr repository.

Adding some basic style

Now we can add some fundamental styling to the details element and its children.

/* Default browser styles for <details> and <summary> */
details { display: block; }
summary { display: list-item; }

/* The following styles will only get applied if JavaScript is enabled and <details> is not natively supported */

/* Add focus styles (for keyboard accessibility) */
.no-details summary:hover, .no-details summary:focus { background: #ddd; }

/* The following styles are not really needed, since the jQuery script takes care of hiding/displaying the elements. */
/* However, we’re still gonna use CSS as well to prevent FOUC in browsers that understand these selectors. */
/* Remember: by default (and probably most of the time), the contents of the <details> element are hidden. */

/* Hide all direct descendants of every <details> element */
/* Note that IE6 doesn’t support the child selector; we’ll work around that using jQuery later */
.no-details details > * { display: none; }

/* Make sure summary remains visible */
.no-details details summary { display: list-item; }

/* Apply a pointer cursor upon hover to indicate it’s a clickable element. These styles can be applied regardless of whether the fallback is needed */
summary { cursor: pointer; }

The jQuery magic

I’ve written a jQuery plugin that makes it very easy to emulate <details>/<summary> in browsers that don’t support these elements yet. Check out the code on GitHub.

After including jQuery and the plugin, you can just write something like this to emulate <details>/<summary> where necessary:

$('details').details();

The result of the feature test is stored in $.fn.details.support, which will be true if the browser natively supports <details> and <summary>, and false otherwise:

// Conditionally add a classname to the `<html>` element, based on native support
$('html').addClass($.fn.details.support ? 'details' : 'no-details');

The plugin will also add the appropriate ARIA annotations for optimal accessibility. This will be done even in browsers that natively support <details>, just in case.

Demo

I’ve put up a demo page with heavily commented source code as well. Enjoy!

This fallback works in all A-grade browsers, including IE6. It will only be executed if the <details> element is not natively supported in the browser. If it isn’t, and JavaScript is disabled, all elements will still be visible to the user.

Note that you should never pull in a 25 KiB JavaScript library just to make <details> work — this solution should only be used in cases where jQuery is used already. Of course it’s possible to rewrite this as plain JavaScript, but I’ll leave that as an exercise to the reader ;)

Image

About me

Hi there! I’m Mathias. I work on Chrome at Google. HTML, CSS, JavaScript, Unicode, performance, and security get me excited. Follow me on Twitter, Bluesky, and GitHub.

Comments

Image

riddle wrote on :

This is a neat demo, exactly what early HTML5 adopters should be doing. Spot on :) I’d like to suggest how it should look. There is no information about this part in the spec, but I’ve seen “disclosure widget” used in another spec – [The Apple HIG](http://developer.apple.com/mac/library/documentation/UserExperience/Conceptual/AppleHIGuidelines/XHIGIntro/XHIGIntro.html "Apple Human Interface Guidelines"). In OS X, this is a disclosure widget: ![Screenshot of a disclosure widget in OS X](/_img/comments/os-x-disclosure-widget.jpg) It’s pretty obvious what it will do when clicked. I think it’s a good starting point.
Image

Remy Sharp wrote on :

Sorry, but I’m going to call you out on the *bulletproof* bit. Why I wouldn’t call this bullet proof: you’re *relying* on jQuery — that’s 25k + your code just to enable `details` by itself — that’s a lot of code for such as small effect (though to your credit — you are suggesting that someone doesn’t include jQuery *just* to produce this effect at the very end of your post). As for the rewrite for sans-jQuery — I wrote this last year and dropped it recently into a gist: [http://gist.github.com/370590](http://gist.github.com/370590)
Image

wrote on :

[**Remy:**](#comment-2) By ‘bulletproof’ I mean it works cross-browser, degrades gracefully without JavaScript and/or CSS, and doesn’t interfere with future native implementations. Requiring jQuery doesn’t make it any less bulletproof, it just adds a dependency. Yes, this script requires jQuery, but it can be rewritten into plain JavaScript, as I indeed noted in my post (and as you already did last year!). I strongly agree my jQuery solution should never be used in the case where jQuery isn’t already included. I’m liking [your version in plain JavaScript](https://gist.github.com/370590/edb6a8f4af75e89bb74eb77dc67dcfcb12ea71e7) — however, unless I’m missing something it doesn’t seem to check for native support, which renders it ‘not bulletproof’ as well. Your implementation is very likely to break when browsers start supporting `
`. FYI, your script doesn’t seem to work in any version of Internet Explorer.
Image

wrote on :

[**Shelley:**](#comment-4) Ah, gotcha! I hadn’t thought of that, thanks. Of course, that didn’t work since `details > * { display: none; }` doesn’t apply to direct child text nodes. This is now fixed. I’ve added two examples to [the demo page](/demo/html5-details-jquery "HTML5
fallback using jQuery: demo") as well to demonstrate this functionality.
Image

wrote on :

[**Shelley:**](#comment-6) Which [ARIA roles](https://www.w3.org/TR/wai-aria/roles#role_definitions "Accessible Rich Internet Applications (WAI-ARIA) 1.0: The Roles Model: Definition of Roles") would you suggest? `role="button"` for the `` element? Which ARIA properties did you have in mind? Should [`aria-expanded`](https://www.w3.org/WAI/PF/aria/states_and_properties#aria-expanded "Accessible Rich Internet Applications (WAI-ARIA) 1.0: Supported States and Properties: the aria-expanded state") be used as an attribute to the `
` element? If [`aria-hidden`](https://www.w3.org/WAI/PF/aria/states_and_properties#aria-hidden "Accessible Rich Internet Applications (WAI-ARIA) 1.0: Supported States and Properties: the aria-hidden state") is used, should it be an attribute to the `
` element? Or should a new element be created to wrap the contents? I [discussed](http://krijnhoetmer.nl/irc-logs/whatwg/20100426#l-796 "IRC logs for the #whatwg channel, 26th April 2010") this with some people with a far better understanding of the WAI-ARIA spec than myself, and [Benjamin Hawkes-Lewis](http://benjaminhawkeslewis.com/) suggested the following:
Image

Shelley wrote on :

What you need to do is ask Ian Hickson and the HTML WG what the ARIA mapping is going to be. Otherwise, we’re all just guessing, because it’s an ill-defined object in the HTML5 spec. What H-L suggests is pretty much what I have for my demonstrations on removing the details element from the HTML spec. You can see four different types at . The two I incorporated were `aria-expanded` and `aria-hidden`. I’m not sure you need a `button` role, because it’s not a button. The `tabindex` should make it keyboard focusable, and NVDA at least says it is clickable. Do not use `section`. Section is an abstract, ontological role. The Illinois Center for Information Technology Accessibility uses roles of `application`, `tab`, and `tablist` for an accordion and tabbed page, and this is nothing more than a singular accordion panel. So I’d incorporate these. Again, though, until this is recorded in the HTML5 spec, what you use may or may not be equivalent to HTML5 details. Unless I can convince the powers-that-be to dump the thing, for the ill-defined thing it is. No offense to your and Remy’s hard work trying to come up with an emulation.
Image

Benjamin Hawkes-Lewis wrote on :

> I’m not sure you need a `button` role, because it’s not a button. Interesting. Seems to me the disclosure clickable is a `button` as WAI-ARIA defines it (“An input that allows for user-triggered actions when clicked or pressed”). Whether the addition of this `role` annotation is actually necessary or helpful is a subtly different question though. > Do not use `section`. Section is an abstract, ontological role. Oops — good point. I think its concrete child [`group`](https://www.w3.org/WAI/PF/aria/roles#group) might be applicable though. > The Illinois Center for Information Technology Accessibility uses roles of `application`, `tab`, and `tablist` for an accordion and tabbed page, and this is nothing more than a singular accordion panel. I find a `tablist` containing only one `tabitem` slightly odd, like an `ol` with a single `li`. Where you have a list (especially a nested list) of `details` elements, WAI-ARIA `tree` and `treeitem` annotations might also be appropriate. While I imagine it might be pretty straightforward to implement `details` natively (for example, as an OS X Disclosure Triangle mapped directly to the `AXDisclosureTriangle` role in the Apple Accessibility API), I don’t think it’s going to be trivial to come up with one satisfactory ARIA mapping that will cover all the ways developers could _fake_ `details` with CSS and JS.
Image

Shelley wrote on :

We really can’t emulate `details`, because there is no accessibility attached to `details`. We’re not even completely sure of the element’s behavior, or what action triggers the behavior. Now, if we’re concerned about the appropriate use of ARIA annotation for existing implementations of this type of functionality, then we’re looking at a different thing. For instance, this is a behavior, but applied to a table, it has one connotation, when applied to form elements, or a menu, there are different connotations. And different ARIA annotations to match. If the item is a popup menu, there is one set of roles and states, as demonstrated here: However, if the context of use differs, then there will be different roles and states. We keep trying to attach semantics to the behavior, when we need to attach semantics to the use. And we won’t know the use, until it’s actually used. Still, we do have states that map to the behavior: `aria-hidden`, `aria-expanded`, `aria-haspopup`, and others. Mathias, I would recommend looking at the code for jQuery UI. It has implemented ARIA states into many of the effects. I bet it could be an excellent guide.
Image

Benjamin Hawkes-Lewis wrote on :

> We really can’t emulate details, because there is no accessibility attached to details. We’re not even completely sure of the element’s behavior, or what action triggers the behavior. > Now, if we’re concerned about the appropriate use of ARIA annotation for existing implementations of this type of functionality, then we’re looking at a different thing. For instance, this is a behavior, but applied to a table, it has one connotation, when applied to form elements, or a menu, there are different connotations. And different ARIA annotations to match. I think that’s right, although I suspect that defining any particular behavioral implementation of `details` semantics would be inappropriate for the HTML5 spec. (Just as with `input type="file"`.)
Image

Ryan wrote on :

Totally going away from where this thread is headed, but aren’t the arrows backwards based on state? The arrows should point down when expanded, not to the right. Vice versa for collapsed.
Image

Steve wrote on :

Really handy technique thank you. Been looking into HTML5 and the worry about a fallback is there.
Image

Webstandard-Blog wrote on :

Very interesting, but I think it will take some time until HTML5 will be the Markup-Language no. 1. Using JavaScript for fixing ‘bugs’ is nice, but you will get some code-overhead you don’t really need.
Image

wrote on :

Despite [**Shelley**](#comment-8)’s objections, [the Working Group decided to keep `
` in the HTML5 spec](https://www.w3.org/Bugs/Public/show_bug.cgi?id=8379#c15) after all.
Image

anon wrote on :

it doesn't scroll down automatically when the content is not visible in the viewport something you should fix
Image

Cam wrote on :

How do you default to open? I tried setting the HTML to `
`, but it seems the script assumes it starts in the closed state. The first click, which should close it, changes that tag to `
` (no attr/class), as it should, but the content stays visible. From then on it works as it should but the content visibility continues to be toggled in the incorrect order.
Image

wrote on :

[**Cam:**](#comment-19) The third example on [the demo page](/demo/html5-details-jquery) uses a `
` element with the `open` attribute. It works for me. Which browser are you testing this in?
Image

Cam wrote on :

[**Mathias:**](#comment-20) Thanks for writing back! I looks like on your demo page, your `
` element with the `open` attribute isn’t working either. I’m using Firefox 3.6.10 (most recent version out at this date). It works fine in Chrome, but in FF it doesn't close the first time clicked, then alternates normally, but showing/hiding the content at the opposite times it should.
Image

wrote on :

[**Cam:**](#comment-21) Ah, I see it now; thanks! I had made some changes to the demo page recently and should’ve tested more thoroughly afterwards. It seems Firefox 3.6.x doesn’t like this line of CSS: .no-details details[open] > * { display: block; } Since this rule is not really necessary anyway (we use JavaScript to take care of displaying/hiding the contents), I just removed it. Works fine now :)
Image

Cam wrote on :

That’s got it! Thanks so much, Mathias. Fantastic work. I love being able to use HTML5 elements with graceful fallback like this.
Image

Prestaul wrote on :

This worked great a week ago, but in the latest version of Chrome (10.0.648.82) the native support detection no longer works properly. `
` nodes now have an `open` property that seems to behave properly (e.g. if you `node.setAttribute('open', 'open');` then `node.open == true`) but there is no visible change to the element. Time to find a new method of detection!
Image

wrote on :

[**Prestaul:**](#comment-25) You’re right, the WebKit team has started implementing `
` and this is the first phase of that. You should note that this only affects nightly builds and beta releases though. The latest stable Chrome release at this time is 9.0.597.102 (not 10.0.648.82). This is a [known ‘bug’ in WebKit](https://bugs.webkit.org/show_bug.cgi?id=51071 "HTML5
and : rendering"). Check out [comment #48](https://bugs.webkit.org/show_bug.cgi?id=51071#c48) which I wrote a couple of days ago: > As long as this patch isn’t landed, WebKit false positives on the feature test for native `
` support (without actually supporting it) […] As long as there’s no stable browser release with this issue, I don’t think we should start looking for a more complicated feature test.
Image

Jonathan wrote on :

Looks as though the stable Chrome 10.0.648.127 has broken this script. I’m not using exactly this method on my own site (I use Prototype) but I’ve put up a [Gist](https://gist.github.com/862069) with a possible change to this one. Sadly it’s based on browser detection, but it shouldn’t catch the latest stable Safari because of the version detection.
Image

wrote on :

[**Jonathan:**](#comment-27) That is very sad news indeed. The solution you have right now is the following: if (!('open' in document.createElement('details')) || ($.browser.webkit && $.browser.version >= 534.16)) { //
is not natively supported } As you mentioned, it’s a shame that we have to rely on browser detection for something as simple as this. The code will have to be edited even more as soon as WebKit fully supports `
`. Let’s see if we can come up with a more robust feature test. Maybe support could be detected by injecting `
foobar
` into the document, checking the height of the `
` element, adding the `open` attribute (or triggering `onclick` on the ``), then checking the height again to see if it changed. If it did, `
` is likely to be supported.
Image

David Mark wrote on :

First off, why would you use jQuery to detect HTML5? jQuery is clearly not appropriate for modern browsers (and Prototype less so) and only a handful of modern browsers have started to implement HTML5. There are so many things wrong with the strategy; it’s hard to know where to start. Mathias has the basic idea, but I sure wouldn’t rely on jQuery’s `height` method. ;) I am glad to see that attribute reflection and inject-and-detect feature testing have finally reached the LCD libraries. You’re welcome. :) But it is disappointing to see developers dropping back to baseless UA sniffing as soon as they hit a perceived wall. Browser sniffing?! More like browser winning! :) Took them five years to get there, but they’ll go back in five minutes. Anything to “save time” and “just get things done”. It’s worse when you have a hundred cooks in the kitchen. BTW, this script that is monitoring my keystrokes is terrible (terribly slow anyway). The only cross-browser keyboard handler on the Web is here: Yes, it’s an add-on for My Library, but there is a slightly lesser generic version located in the Primers section. As one might expect, both are fast. Accurate, too. And, wonder of wonders, no browser sniffing and no broken or “unsupported” browsers in its wake. Look at the typical “framework” for help with keyboard input and you find lots of browser sniffs, abominable performance and virtually no explanation of their shortcomings. I’m sure whatever jQuery plug-in is used here is of similar “quality”. And, as a user, I’m complaining about it. Never mind the script; the rendered behavior is headache-inducing.
Image

David Mark wrote on :

And dammit, I can’t believe you are trying to use the `attr` method for this. Of all things. :) Don’t do what Resig did (e.g. try to copy the code without understanding it). If you think about what you are doing, you will realize that you don’t need a wrapper for this at all. PS. There’s nothing self-documenting (or magic) about jQuery code. Think about it.
Image

wrote on :

Native support for `
` and `` [just landed in WebKit](http://trac.webkit.org/changeset/81035). `` elements aren’t keyboard-navigable yet, but accessibility issues will be handled in future patches. What’s exciting is that the native behavior looks almost identical to the demo page :)
Image

wrote on :

[**Rudie:**](#comment-32) That’s effectively the same as checking `'open' in document.createElement('details')`.
Image

Cam wrote on :

It looks like Chrome 10 has implemented `
` with an `open` attribute, but the functionality doesn’t work yet. So your detection code things Chrome’s good to go even though it’s not. Is there a more consistent way to detect if the functionality works?
Image

wrote on :

[**Cam:**](#comment-34) You may wanna read the above comments, starting at [comment 25](#comment-25). I describe a possible feature test for `
` support. Your comment convinced me I should write it out, so here goes: ``` var isDetailsSupported = (function(doc) { var el = doc.createElement('details'), fake, root, diff; if (!('open' in el)) { return false; } root = doc.body || (function() { var de = doc.documentElement; fake = true; return de.insertBefore(doc.createElement('body'), de.firstElementChild || de.firstChild); }()); el.innerHTML = 'ab'; el.style.display = 'block'; root.appendChild(el); diff = el.offsetHeight; el.open = true; diff = diff != el.offsetHeight; root.removeChild(el); if (fake) { root.parentNode.removeChild(root); } return diff; }(document)); ``` The `isDetailsSupported` variable will be `true` if `
` is natively supported, and `false` if not. Feel free to use the above snippet as you please. It’s public domain, really. **Update:** This code snippet has since been [included in has.js](https://github.com/phiggins42/has.js/blob/6a7d67108b32fc1811fad8bafb14b2d9313c86dc/detect/features.js#L154-176) and is available as a [Modernizr plugin](https://github.com/Modernizr/Modernizr/blob/master/feature-detects/elem-details.js) as well. Some notes: * As you can see, the script will automatically create a new `` element if there isn’t one already (i.e. when the script is called from the ``). This is done to make sure the test `
` element is visible so we can measure its height. The fake `` element will be removed from the DOM afterwards. This technique is used a lot in [has.js](https://github.com/phiggins42/has.js), the à la carte feature detection library. * The snippet creates a new `
` element and sets its `display` CSS property to the `block` value. This is done for two reasons. Firstly, most browsers that don’t support rendering `
` and ``, treat these elements as inline elements (`display: inline`). If we don’t override that, there’s no way to get reliable results for the height in all browsers. Secondly, this makes sure the test will still return the correct result when the document has `details { display: none; }` in its style sheet. (Edge case, but still.)
Image

Mark wrote on :

Your code seems to break with the new jQuery 1.6.1. With 1.6.0 the fallback in FF etc. works just fine, but with the new jQuery version the `open` attribute gets ignored. `open = $details.prop('open');` (as they suggested for boolean values) doesn’t work either…
Image

Carlos Ramirez III wrote on :

I was experienced an issue where the `open` attribute wasn’t being removed upon collapsing the `details` element. It seemed like the call to `$details.attr('open')` was always returning `undefined`. I replaced that expression with `$details.is('[open]')` instead and that seemed to fix the issue. Did a quick check on a bunch of different browsers and it seemed to work correctly. Hope that can help anyone else seeing the same issue.
Image

Stephan Sokolow wrote on :

Your implementation of the `open` attribute seems to be broken in Firefox 6. Works in Chromium 13.0.782.107’s native `details` support but all examples display collapsed in Firefox 6 once I tell NoScript to allow JS.
Image

Daniel wrote on :

Thanks for this script it works great, bar one thing. If there are any `