Fixing the details Element

James Edwards
James Edwards
Share

The HTML5 <details> element is a very neat construct, but it also has quite a serious usability problem – what happens if you follow a hash-link which targets inside a collapsed <details> element? The answer is nothing. It’s as if the target was hidden. But we can fix that issue with a little progressively-enhanced JavaScript, and an accessible polyfill for browsers without native support.

Introducing <details>

If you’re not already familiar with the details and summary elements, here’s a quick example:
<details open="open">
  <summary>This is the summary element</summary>
  <p>
    This is the expanding content
  </p>
</details>
The <summary> element, if present, must be the first or last child. Everything else is considered to be the content. The content is collapsed by default unless the open
attribute is defined. Native implementations update that attribute when the user clicks the summary to open and close it. Currently, only Chrome supports the <details> tag. The following figure shows how Chrome renders the previous example.
The details and summary element in Chrome

The details and summary element in Chrome

It’s no different than normal text, except for the small triangle, referred to as a discloure triangle. Users can open and close it by clicking the triangle, or anywhere inside the <summary> element. You can also Tab
to the summary and press Enter.

Creating a Polyfill

It’s pretty straightforward to implement a basic polyfill to emulate the <details> tag. The polyfill identifies native implementations by the existence of the open property – a DOM mapping of the open
attribute. In native implementations, we don’t have to manually update the open attribute, but we do still have to update its ARIA attributes, which are based on the following structure.
<details open="open">
  <summary>This is the summary element</summary>
  <div>
    <p>
      This is the expanding content
    </p>
  </div>
</details>
The inner <div> is the collapsing content. The script binds an aria-expanded
attribute to that element, which switches between true and false when the element is opened and closed. The attribute is also used as a CSS selector (shown below), which visually collapses the content using display.
details > div[aria-expanded="false"]
{
  display:none;
}
Now we don’t really need
a wrapping content element, but without that we’d have to set aria-expanded and display on each inner element individually – which is more work, and could be rather inconvenient if the elements have different display properties. This is especially true in IE7! For some reason, IE7 doesn’t apply the display change when the user manually opens and closes it. However, it does apply it by default (which proves it understands the selector), and the change in attribute value can be seen in the DOM. It’s as though it can apply the selector, but not un-apply it again. For that reason, we have to define a style.display change too, which makes it particularly convenient to have a content element; and since we have to do that for IE7, we end up getting IE6 support for free! The only other significant thing to note in the polyfill is the addClickEvent
abstraction, which handles the difference between browsers which fire keyboard click events, and those that don’t:
function addClickEvent(node, callback)
{
  var keydown = false;
  addEvent(node, 'keydown', function()
  {
    keydown = true;
  });
  addEvent(node, 'keyup', function(e, target)
  {
    keydown = false;
    if(e.keyCode == 13) { callback(e, target); }
  });
  addEvent(node, 'click', function(e, target)
  {
    if(!keydown) { callback(e, target); }
  });
}
For elements like links and buttons, which natively accept keyboard focus, all browsers fire the click event when you press the Enter
key. But, our <summary> elements only accept the focus because we added tabindex, and here the situation varies by browser. It’s really only the difference that’s a problem – if all browsers behaved one way or the other, things would be simple. But, since there are different behaviors we have to use a little cunning. So, we define keydown and keyup
events to handle the Enter key. The events also set and clear a flag which the click event then refers to, so it can ignore duplicate keyboard events while handling mouse and touch events.

Highlighting the Hash Problem

So now we’ve got a functional polyfill, let’s link to that example again, but this time including a fragment identifier (i.e. a hash link) that points to the ID of the first element’s content:
Since the target element is inside a collapsed region, the page never jumps to that location – it stays at the top of the page while the target remains hidden. In most cases, a user wouldn’t understand what happened there. Perhaps they might scroll down, click stuff, and eventually find what they were looking for, but this is not good usability. A worse example of the same issue arises when clicking an internal hash link – if the target is inside a collapsed region, the link will do nothing at all. Happily though, this is a case that’s easy to describe, and therefore easy to define the logic that addresses it:
  • If the hash matches the ID of an element on this page, and that element is inside (or is) a <details> element, then automatically expand the element, and any identical ancestors
Once we’ve implemented that, we’ll get much better behavior, as the details region automatically expands to expose the location target:

Fixing the Hash Problem

We can fix the hashing problem with the following recursive function.
function autostate(target, expanded, ancestor)
{
  if(typeof(ancestor) == 'undefined')
  {
    if(!(target = getAncestor(target, 'details')))
    {
      return null;
    }
    ancestor = target;
  }
  else
  {
    if(!(ancestor = getAncestor(ancestor, 'details')))
    {
      return target;
    }
  }

  statechange(ancestor.__summary, expanded);

  return autostate(target, expanded, ancestor.parentNode);
}
The function accepts a target element and the expanded=false state flag, and will identify whether the target is inside a <details> element. If so, it passes its <summary> element (saved as a local __summary
property) to the statechange function, which applies the necessary changes to expand the element. Next, recur up the DOM to do the same thing with any ancestors, so that we can handle nested instances. We need to have separate arguments for the original target and subsequent ancestors, so we can return the original target at the end of all recursions, i.e. if the input target was inside a collapsed region, the same target is returned, otherwise null is returned. We can then call autostate from click
events on internal page links, as well as calling it at page load for the element matched by location.hash:
if(location.hash)
{
  autostate(document.getElementById(location.hash.substr(1)), false);
}
Originally, I wanted that to be all the function does – get the target, expand its containers, then let the browser jump to its location. But, in practice that wasn’t reliable because in order to make it work, the elements had to be expanded before the link was clicked, otherwise the browser wouldn’t jump to the target location. I tried to fix that by preempting the link action using separate mousedown, keydown
, and touchstart events, so the target would already be expanded before the link is followed. Unfortunately, that was very convoluted and it still wasn’t reliable! So, eventually I found that the best approach was to auto-scroll the browser using the window.scrollBy function, before still returning true on the link so the address bar is updated. This is where we need the target reference (or lack of it) returned by the autostate
function – if it returns a target then scroll to the target’s position:
if(target = autostate(document.getElementById('hash'), false))
{
  window.scrollBy(0, target.getBoundingClientRect().top);
}
Using the getBoundingClientRect function provides the perfect data, since it tells us the position of the target element relative to the viewport (i.e. relative to the part of the document you can see inside the browser window). This means it only scrolls as far as necessary to find the target, and is why we use scrollBy instead of scrollTo
. But, we don’t do that when handling the default location.hash, in order to mirror native browser behavior with ordinary hash links – when you refresh a page with a location hash, the browser doesn’t jump back to the target location, it only does that the first time the page loads. So, to get that behavior, we musn’t auto-scroll for location targets. Instead, we must allow the native jump to happen at the appropriate time. We achieve this by deferring the script’s initialization with DOMContentLoaded (plus a backup onload for older browsers), which means that the page has already
jumped to the target location, before the script collapses its containing regions in the first place.

Conclusion

I think of scripting like this as an omnifill. It’s more than just a polyfill for browsers without the latest features, as it also enhances the usability and accessibility of the features themselves, even in browsers which already support them. The download files for the examples in this article are listed below.

Frequently Asked Questions (FAQs) about the HTML Details Element

What is the HTML Details Element and how does it work?

The HTML Details Element is a unique HTML tag that allows you to create an interactive widget that users can open or close to reveal or hide additional information. It works in conjunction with the Summary Element, which provides a clickable title for the Details Element. When a user clicks on the Summary, the Details Element expands to reveal the content within it. This is particularly useful for creating FAQs, product descriptions, or any content that benefits from a collapsible interface.

How can I set the Details Element to open by default?

By default, the Details Element is closed. However, you can set it to open by default by adding the ‘open’ attribute to the Details tag. For example: <details open>. This will ensure that the content within the Details Element is visible when the page loads.

Can I style the Details and Summary Elements with CSS?

Yes, you can style the Details and Summary Elements using CSS. Both elements can be targeted using their respective tag names in your CSS. For example, you can change the font size of the Summary Element like this: summary {font-size: 20px;}. You can also style the Details Element when it’s open using the :open pseudo-class.

Can I nest multiple Details Elements within each other?

Yes, you can nest multiple Details Elements within each other to create a multi-level collapsible content structure. However, keep in mind that each nested Details Element must have its own Summary Element.

Can I use JavaScript with the Details Element?

Yes, you can use JavaScript with the Details Element to control its behavior. For example, you can use the toggle event to execute a function whenever the Details Element is opened or closed.

Is the Details Element supported in all browsers?

The Details Element is supported in most modern browsers, including Chrome, Firefox, Safari, and Edge. However, it’s not supported in Internet Explorer. For maximum compatibility, consider using a polyfill or fallback solution.

Can I use the Details Element for forms?

Yes, the Details Element can be used in forms to group related input fields and provide additional information without cluttering the form. However, remember that the Details Element does not have any semantic meaning in forms, so it should not replace fieldset and legend elements for grouping and labeling form controls.

Can I use the Details Element for navigation menus?

Yes, the Details Element can be used to create collapsible navigation menus. However, keep in mind that the Details Element is not a replacement for the Nav Element, and should be used in conjunction with it for semantic correctness.

Can I use the Details Element for footnotes?

Yes, the Details Element can be used to create interactive footnotes. The Summary Element can contain the footnote reference, and the Details Element can contain the footnote text. This allows users to view the footnote content on demand, without leaving their current reading position.

Can I use the Details Element for comments or reviews?

Yes, the Details Element can be used to create collapsible comments or reviews. The Summary Element can contain the comment author and date, and the Details Element can contain the comment text. This allows users to browse comments or reviews in a compact and organized manner.