More accessible markup with display: contents

CSS Grid Layout lets you turn an element into a grid, and place the element’s direct children onto it. Given that, it might be tempting to use flatter markup, but less meaning is usually less accessibility. With display: contents, we can place grand children on a grid, which lets us have accessible markup and beautiful layout. Let’s dive into the details!

Below, I will explain in more detail what I mean by children and grand children, and then show how we can use display: contents to improve this. Note: this caused an accessibility bug in all major browser engines, which has been addressed partially in all major engines since May 2022, with open issues, see below for details

Grid works on direct children

In Grid Layout, when a grid is defined on a given element, only direct children of that element become grid items and are layed out on it. To refresh for those not familiar with the syntax, let’s look at an example and write a recipe. With this HTML:

Example layout 1
<div class="container">
  <h1 class="item">Penne with tomato sauce</h1>
  <p class="item">This simple recipe has few ingredients but tastes delicious.</p>
  <div class="item ingredients">
    <h2>You'll need</h2>
    <ul>
      <li>canned tomatoes</li>
      <li>onions</li>
      <li>garlic</li>
    </ul>
  </div>
</div>

we can have this CSS:

.container { 
  display: grid; /* element is now a grid container */
  grid-template-columns: repeat( 4, 1fr );  /* grid now has 4 columns */

.item:nth-child(1) {
  grid-columns: 1 / 2; /* Place item between grid line 1 and 2 */
}

.item:nth-child(2) {
  grid-columns: 2 / 4; /* Place item between grid line 2 and 4 */
}

.item:nth-child(3) {
  grid-columns: 4 / 5; /* Place item between line 4 and 5 */
}

I’ve used .container and .item as classnames, because that’s core to Grid Layout: there’s grid containers and grid items. Obviously, use any naming convention your projects require.

The reason we can position these items on the grid, is that they are direct children of the grid container. But look at what happens if we’d like to add a list of sponsors, like this:

Example layout 2

We could add the list to our markup:

<div class="container">
  <h1 class="item">Penne with tomato sauce</h1>
  <p class="item">This simple recipe has few ingredients but tastes delicious.</p>
  <div class="item ingredients">
    <h2>You'll need</h2>
    <ul>
      <li>canned tomatoes</li>
      <li>onions</li>
      <li>garlic</li>
    </ul>
  </div>
  <ul class="item sponsors">
    <li>Supermarket 1</li>
    <li>Supermarket 2</li>
  </ul>
</div>

But we won’t be able to position each sponsor onto the grid. This is because only the <ul> is a direct child of the container element and therefore a grid item. The <li>s are not: because they are not direct children of our grid container, they don’t get to participate in its grid game. But what if we really want to align the sponsors onto the grid?

Flatter markup

One obvious method of making the sponsors participate is to remove the <ul> and use a <div> for each sponsor. But what we would then do, it ‘flatten’ our markup. That’s throwing away the baby with the bathwater.

The benefit of using the <ul> (unordered list) element are plenty:

  • it will stay a list outside the context of our page, for example in Safari Reader mode, it will show as a list
  • when printed with stylesheet turned off, it will show as a list
  • for people who use screenreaders, it is a list (screenreaders can announce things like ‘list, 3 items’).

If we would make our markup flatter, we lose those benefits.

display: contents to the rescue

With display: contents, we can have our markup and our grid placement. This property makes the element so that it no longer seems to exist. It does not generate a box, so backgrounds, borders and other box-related properties will no longer work on it. Grid placement properties will also no longer work. But all of these things will work for the element’s children. The spec says the element behaves ‘as if it had been replaced […] by its contents’. If that sounds weird, I can recommend Ire Aderinokun’s detailed explainer.

Without display contents on ul it is a grid item, with display contents its children are grid items

Effectively, for our purposes here, using display: contents on an element does this: the element stops participating in the grid, and its contents start participating in it. It lets us specify our sponsors onto the grid, instead of the list they are contained in.

There’s some interesting edge cases listed in the spec, for if the property is used on elements like img and video.

Accessibility concerns with current browser implementations of display: contents

For people who use assistive technologies (AT), browsers expose accessibility properties, including the role of elements on the page. This is so that their AT knows what’s what on the page. Many elements come with a built-in role, for example lists have a role of list.

This is where it goes wrong in current browsers that support display: contents. They do not interpret display: contents as a lay-out thing only, they also derive meaning from it. This is problematic and a bug according to the spec’s comment on display affecting layout:

The display property has no effect on an element’s semantics: these are defined by the document language and are not affected by CSS. Aside from the none value, which also affects the aural/speech output and interactivity of an element and its descendants, the display property only affects visual layout: its purpose is to allow designers freedom to change the layout behavior of an element without affecting the underlying document semantics.

(emphasis mine)

Looking at our sponsor list example, it means that the item is no longer seen as a list, but as something else (test case in CodePen).

I’ve added my test results per browser below. In each of them, our <ul> gets the correct role without display: contents, but once the property is set, it loses its role.

Firefox 61

The list gets a role of text leaf (Firefox bug). Update: this is now fixed; it will ship in Firefox 62 in August 2018

Chrome 66

The list shows as ‘accessibility node not exposed, element not rendered’ (Chromium bug). Update: this is now fixed; it has shipped in Chome 89 in March 2021

Safari

The list shows as ‘no accessibility information’ (Safari bug, Webkit bug #185679, Webkit bug #237834 that they did more work in)

Update 13 June 2022: this was reported fixed in Safari TP 144 / Safari 16
Update 9 July 2022: Adrian Roselli reports work is still needed in Safari

Related to this issue is that display properties in CSS have impact on table semantics, as Adrian Roselli explains; see also Steve Faulkner’s explanation of where the responsibilities lie.

Conclusion

With display: contents, you can place grand children of a grid container on the grid. This allows for more semantic mark-up, which is great for accessibility. The more meaningful your mark-up, the more detail an assistive technology can provide to its users. However, there is one caveat: none of the browsers that currently support display: contents expose elements that have the property to the accessibility tree with their original role.

I believe the accessible roles should not disappear when setting display: contents, as that defeats a lot of the purpose of display: contents. I have filed bugs with Firefox, Chromium and Safari for this. I really hope we will be able to use display: contents while keeping the accessible roles of elements intact, so that we can have great layouts and great accessibility. To be continued, I hope!

Update August 2018: the bug was fixed in Firefox 62

Update March 2021: the bug was fixed in Chrome 89

Update May 2022: initial fix was announced for Safari Technology Preview 144

Update June 2022: reflected that bug fix will be in Safari 16

Update July 2022: added links to Adrian Roselli's latest tests

Comments, likes & shares (115)

Greg Whitworth, Lasha Krikheli, Accessabilly, Saptak S, Tyler Wilcock, Matthias Ott, August.SQL, Oliver Davies, Laura Whitehead, Rich Senior 💙🇺🇦, Krystal Higgins, Roel Groeneveld, Jacob Pellegren, theAdhocracy, Egor Kloos, Anneke Sinnema, Obscure Hooligan, Scott McCracken, John D. Jameson, Tadeu Cruz, Paco Pistolas, Deirdre Saoirse Moen, Stefan Galescu, James Hunter, Mark Root-Wiley, Luce Carević, Michelle Barker, Carlos Espada, Konstantin, Paul Hachmang, Pim, Bram Smulders, Swetha P, Seth A. Roby, Bruno Stasse, Florian Geierstanger, Matt Bainton | ✊🏽🇺🇦, Josh Coody, Accessabilly, Jen Simmons, Suzanne Aitchison, Jose Farias, Matthias Ott, Daniel Göransson, nguythang, Eugene, Martin Auswöger, Karl Stolley, Steven Beshensky, Arturo Castillo Delgado, Vi, Martin Sangolt 🏳️‍🌈, Kai, Sven Wolfermann, Diego Ballesteros, Steve Lee, Matsuko, Michaël Vanderheyden, DR0P D TECHN0, Alistair Shepherd, Captain of the CSS Enterprise 🌍🌹🕊️🌈🖖, Ted Barnes, Sean Voisen, Roland Franke, Libor Vaněk, Mox, Roma Komarov, Andrew Millar, Huw, Yvonne Astrocat, Francis Rubio #AtinAngKulayaan, Daniel Montalvo, MarcelloP, Joe Dolson, Cristian Díaz, 🔴 Christian Schaefer, Ebn Sina, Daniel Davis, Derk-Jan 💙💛🇺🇦, Florian Sanders, Raed Salah, Ash, XO53 and Access42 liked this

Greg Whitworth, Jeremy Keith, Jerry Lee 🦀, Allen should still be masking Hoffman, Jen Simmons, Carlos Espada, Benaiah, Deirdre Saoirse Moen, Joe Dolson, Cristian Díaz, Svenja, Florian Sanders and Jonathan Dallas reposted this

@hdv hello there! there's a new safari TP (webkit.org/blog/12621/rel…) with the fix for bugs.webkit.org/show_bug.cgi?i… => would you check hidde.blog/more-accessibl… again? :) bugs.webkit.org/show_bug.cgi?i… is still open, so we are probably somewhere in between, but…
This is really awesome! Haven't used display: contents before. Thanks for sharing.
Took them long enough, I really like this feature and have used it knowing full well it wasn’t accessible. I’ll burn in hell for that one.
I saw this one too! This bug hit me more than once.
This is an example of why I don’t trust anything until I’ve verified it myself Not because I think people are being dishonest. But because people ship inaccessible code all the time, even when they think they haven’t.
Here’s a great writeup on the issue by @hdv. A great example of ‘display:contents’ affecting children is when the element with that style also has a role, like ‘list’. hidde.blog/more-accessibl…