Bulletproof HTML5 <details> fallback using jQuery
The HTML5 <details> element is currently not natively supported in any browser. 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
detailselement 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
summaryelement represents a summary, caption, or legend for the rest of the contents of thesummaryelement’s parentdetailselement, if any. If there is no childsummaryelement, the user agent should provide its own legend (e.g. “Details”).
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 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 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>
if (!('open' in document.createElement('details'))) {
document.documentElement.className += ' no-details';
};
</script>
Of course, you should minify this:
<script>if(!('open' in document.createElement('details')))document.documentElement.className+=' no-details'</script>
Note that Modernizr currently doesn’t detect <details> support. I’m sure it will be included soon.
Adding some basic style
Now we can add some fundamental styling to the details element and its children.
/* <details> and <summary> are block level elements */
details, summary { display: block; }
/* 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; }
/* Hide all direct descendants of every <details> element */
/* Note that IE6 doesn’t support the child selector; we’ll fix that later */
.no-details details > * { display: none; }
/* Display all direct descendants of every <details> element with an `open` attribute */
/* Note that IE6 doesn’t support the attribute selector; we’ll fix that later */
.no-details details[open] > * { display: block; }
/* Make sure summary remains visible, and apply a pointer cursor upon hover to indicate it’s a clickable element */
.no-details details summary { display: block; cursor: pointer; }
As you can see, not all of these CSS rules are cross-browser compatible; IE6 chokes on two of them. Basically, we’re gonna use jQuery and its selector engine instead to force these styles to IE. We’ll get to that later.
The jQuery magic
There’s not much to explain here. As they say, “good code is self-documenting” — and hey, I basically explain everything that happens in comments. Check it out:
$(function() {
// Execute the fallback only if there’s no native `details` support
if (!('open' in document.createElement('details'))) {
// Loop through all `details` elements
$('details').each(function() {
// Store a reference to the current `details` element in a variable
var $details = $(this),
// Store a reference to the `summary` element of the current `details` element (if any) in a variable
$detailsSummary = $('summary', $details),
// Do the same for the info within the `details` element
$detailsNotSummary = $details.children(':not(summary)'),
// This will be used later to look for direct child text nodes
$detailsNotSummaryContents = $details.contents(':not(summary)');
// If there is no `summary` in the current `details` element…
if (!$detailsSummary.length) {
// …create one with default text
$detailsSummary = $(document.createElement('summary')).text('Details').prependTo($details);
};
// Look for direct child text nodes
if ($detailsNotSummary.length !== $detailsNotSummaryContents.length) {
// Wrap child text nodes in a `div` element
$detailsNotSummary = $detailsNotSummaryContents.wrap('<div>').parent();
$details.attr('open') ? $detailsNotSummary.show() : $detailsNotSummary.hide();
};
// Set the `tabindex` attribute of the `summary` element to 0 to make it keyboard accessible
$detailsSummary.attr('tabindex', 0).click(function() {
// Focus on the `summary` element
$detailsSummary.focus();
// Toggle the `open` attribute of the `details` element
$details.attr('open') ? $details.removeAttr('open') : $details.attr('open', 'open');
// Toggle the additional information in the `details` element
$detailsNotSummary.toggle(0);
}).keyup(function(event) {
if (13 === event.keyCode || 32 === event.keyCode) {
// Enter or Space is pressed — trigger the `click` event on the `summary` element
// Opera already seems to trigger the `click` event when Enter is pressed
if (!($.browser.opera && 13 === event.keyCode)) {
event.preventDefault();
$detailsSummary.click();
};
};
});
});
// Make the following CSS work in IE6:
// details > * { display: none; }
// details[open] > * { display: block; }
// Note that $('details').children() is faster than $('details > *')
if ($.browser.msie && $.browser.version < 7) {
$detailsNotSummary.hide();
$('details[open]').show();
};
};
});
There are, however, a few things to note.
For example, you might wonder why I’m using $(document.createElement('summary')) instead of just $('<summary>'). There are two reasons for this:
- The former is faster and more efficient.
- The latter fails to work in IE6. I did some tests
alert()ingdocument.body.innerHTMLafter running the fallback code in that browser, and it seems like$('<summary>')creates a<SUMMARY>node (which can’t be styled by default in IE) whereas$(document.createElement('summary'))simply generates a<summary>element, which works flawlessly.
At one point, I’m using .toggle(0) instead of just .toggle(). That’s just to make it work in Webkit browsers, who wouldn’t display the <details> information — for some reason, the initial CSS styles seemed to have a higher specificity. Adding a duration parameter, i.e. .toggle(0) seems to do the trick.
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 IE 6. 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 ;) Update: Looks like Remy Sharp already wrote a sans-jQuery fallback for <details> with similar functionality. Nice!
Comments
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. In Mac OS X, this is a disclosure widget:
It’s pretty obvious what it will do when clicked. I think it’s a good starting point.
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
detailsby 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
Mathias wrote on :
Remy: 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 — 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
<details>.As mentioned in the article, you can use the following code to check if
<details>is supported natively:Shelley wrote on :
Did you test with a
detailselement that only contains text?Mathias wrote on :
Shelley: 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 as well to demonstrate this functionality.
Shelley wrote on :
Cool, but where’s the ARIA annotation?
Mathias wrote on :
Shelley: Which ARIA roles would you suggest?
role="button"for the<summary>element?Which ARIA properties did you have in mind? Should
aria-expandedbe used as an attribute to the<details>element? Ifaria-hiddenis used, should it be an attribute to the<details>element? Or should a new element be created to wrap the contents?I discussed this with some people with a far better understanding of the WAI-ARIA spec than myself, and Benjamin Hawkes-Lewis suggested the following:
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 http://burningbird.net/html5/.
The two I incorporated were
aria-expandedandaria-hidden. I’m not sure you need abuttonrole, because it’s not a button. Thetabindexshould 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 ofapplication,tab, andtablistfor 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.
Benjamin Hawkes-Lewis wrote on :
Interesting. Seems to me the disclosure clickable is a
buttonas WAI-ARIA defines it (“An input that allows for user-triggered actions when clicked or pressed”). Whether the addition of thisroleannotation is actually necessary or helpful is a subtly different question though.Oops — good point. I think its concrete child
groupmight be applicable though.I find a
tablistcontaining only onetabitemslightly odd, like anolwith a singleli.Where you have a list (especially a nested list) of
detailselements, WAI-ARIAtreeandtreeitemannotations might also be appropriate.While I imagine it might be pretty straightforward to implement
detailsnatively (for example, as an OS X Disclosure Triangle mapped directly to theAXDisclosureTrianglerole 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 fakedetailswith CSS and JS.Shelley wrote on :
We really can't emulate
details, because there is no accessibility attached todetails. 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: http://test.cita.illinois.edu/aria/menubar/menubar1.php
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.Benjamin Hawkes-Lewis wrote on :
I think that's right, although I suspect that defining any particular behavioral implementation of
detailssemantics would be inappropriate for the HTML5 spec. (Just as withinput type="file".)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.
Mathias wrote on :
Ryan: D’oh! Thanks, it’s fixed now.
Steve wrote on :
Really handy technique thank you. Been looking into HTML5 and the worry about a fallback is there.
Bramus! wrote on :
Neat!
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.
Mathias wrote on :
Despite Shelley’s objections, the Working Group decided to keep
<details>in the HTML5 spec after all.anon wrote on :
it doesn't scroll down automatically when the content is not visible in the viewport something you should fix