Mathias Bynens

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 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”).

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:

  1. The former is faster and more efficient.
  2. The latter fails to work in IE6. I did some tests alert()ing document.body.innerHTML after 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:

Screenshot of a disclosure widget in Mac OS X

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 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

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:

if ('open' in document.createElement('details')) {
// Yay, <details> is natively supported!
} else {
// No support for <details> — fallback goes here
};

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.

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-expanded be used as an attribute to the <details> element? If aria-hidden is 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:

<details role="section" aria-expanded="false" aria-labelledby="label">
<summary id="label" role="button" aria-controls="content">Summary goes here</summary>
<div id="content" aria-hidden="true">Content goes here</div>
</details>

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-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.

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 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.

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: 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 :

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".)

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.

Steve wrote on :

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

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.

anon wrote on :

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

Leave a comment

Comment on “Bulletproof HTML5 <details> fallback using jQuery”

Some Markdown is allowed; HTML isn’t. Keyboard shortcuts are available.

It’s possible to add emphasis to text:

_Emphasize_ some terms. Perhaps you’d rather use **strong emphasis** instead?

Select some text and press + I on Mac or Ctrl + I on Windows to make it italic. For bold text, use + B or Ctrl + B.

To create links:

Here’s an inline link to [Google](http://www.google.com/).

If the link itself is not descriptive enough to tell users where they’re going, you might want to create a link with a title attribute, which will show up on hover:

Here’s a [poorly-named link](http://www.google.com/ "Google").

Use backticks (`) to create an inline <code> span:

In HTML, the `p` element represents a paragraph.

Select some inline text and press + K on Mac or Ctrl + K on Windows to make it a <code> span.

Indent four spaces to create an escaped <pre><code> block:

    printf("goodbye world!");  /* his suicide note
was in C */

Select a block of text (more than one line) and press + K on Mac or Ctrl + K on Windows to make it a preformatted <code> block.

Quoting text can be done as follows:

> Lorem iPad dolor sit amet, consectetur Apple adipisicing elit,
> sed do eiusmod incididunt ut labore et dolore magna aliqua Shenzhen.
> Ut enim ad minim veniam, quis nostrud no multi-tasking ullamco laboris
> nisi ut aliquip iPad ex ea commodo consequat.

Select a block of text and press + E on Mac or Ctrl + E on Windows to make it a <blockquote>.