Making Wrappers Transparent for Grid and Flexbox
Sometimes a wrapper element gets in the way: it breaks the grid line, prevents a direct flex child relationship, or forces an extra layout level. display: contents makes the element invisible to the layout context, its children step in directly. What sounds simple, however, comes with serious accessibility pitfalls that everyone using this feature in production needs to know.
Table of Contents
- 1. What display: contents Does and Does Not Do
- 2. Grid pass-through: children directly on the grid track
- 3. Flexbox pass-through: nested flex children
- 4. The accessibility bugs: what breaks in practice
- 5. ARIA and display: contents: what the spec says
- 6. Browser bugs and known incompatibilities
- 7. Safe use cases for display: contents
- 8. Alternatives: subgrid and other approaches
- 9. display: contents in direct comparison
- 10. Summary
- 11. FAQ
1. What display: contents Does and Does Not Do
display: contents removes an element's box from the layout model but keeps its children and content intact. The element itself no longer has a box: no margin, no padding, no border (these properties are ignored), no background color. It behaves as if it were not there, but only for the layout context. The element remains in the DOM and in the accessibility tree (with important exceptions covered in the accessibility section). Its children participate directly in the layout context of the nearest real ancestor.
A concrete example: a <div class="wrapper"> with display: contents inside a grid container does not itself appear as a grid item. Instead, its children become direct grid items of the parent grid. This opens the door to markup structures where semantic wrapper elements, which have to be in the DOM for content or JavaScript reasons, do not interfere with the layout. What display: contents does not do: it does not remove the element from the DOM, it does not remove it from JavaScript selector queries, and in properly implemented browsers it does not remove it from the accessibility tree.
2. Grid pass-through: children directly on the grid track
The most common use case for display: contents is passing wrapper elements through in CSS Grid. Suppose a table-like view is implemented with Grid, and the data comes from an API as structured groups: each row is an object with several fields, and in the JavaScript framework each row is rendered as a component that outputs a wrapper div. Without display: contents, the grid layout breaks because the wrapper div is interpreted as a grid item and the inner fields start a new layout context.
With display: contents on the wrapper div, the inner fields become direct grid items of the parent grid. The grid tracks run straight through the wrapper as if it were not there. This makes complex, data driven layouts possible where the markup is generated by frameworks that do not allow direct control over the final HTML structure, such as the Magento block system, React component libraries, or server rendered lists. In these cases display: contents is often the only solution without an HTML structure refactor.
/* display: contents: Grid pass-through pattern */
/* Grid container: 3 columns for date | title | category */
.article-list {
display: grid;
grid-template-columns: 8rem 1fr 10rem;
gap: 0 1.5rem;
align-items: baseline;
}
/* Wrapper rendered by framework, does NOT create a grid item */
.article-list__group {
display: contents; /* transparent to grid, children become grid items */
}
/* Children participate directly in the parent grid */
.article-list__date { color: #7c3aed; font-variant-numeric: tabular-nums; }
.article-list__title { font-weight: 600; }
.article-list__category { color: #64748b; font-size: 0.875rem; }
/* Without display: contents: */
/* .article-list__group would be a single grid item spanning all 3 columns */
/* The inner children would create their own flow layout inside */
/* Row separator via pseudo-element on the title cell */
.article-list__title::after {
content: '';
display: block;
height: 1px;
background: #e2e8f0;
grid-column: 1 / -1; /* only works if title is a direct grid child */
margin-top: 0.75rem;
}
3. Flexbox pass-through: nested flex children
display: contents works analogously in flexbox contexts. A wrapper element with display: contents inside a flex container is skipped over, its children become direct flex items. This is especially useful for semantic groupings in navigation bars or toolbar elements, where items are grouped together for structural reasons but should still be aligned with flexbox as if they were direct siblings.
A common example: a horizontal navigation with a <ul> as a flex container, but certain navigation items are grouped inside a <div> for JavaScript purposes (for example, for a dropdown menu). With display: contents on the wrapper div, all <li> elements remain direct flex items, keeping their flex gaps and alignment as if the wrapper did not exist. The JavaScript code can still use the wrapper for event handling and state management. The layout looks as if there were no wrapper, and as far as CSS is concerned, there is not.
4. The accessibility bugs: what breaks in practice
The most serious limitation of display: contents is its well known accessibility bugs. The most critical one: in some versions of Chrome and Firefox, an element with display: contents was removed from the accessibility tree as if it really were not there. This does not just affect visual presentation, it affects the semantics that screen readers rely on. A <button> with display: contents was not recognizable as a button to screen readers in the affected browser versions. An <a> element lost its role as a link. That is a critical accessibility problem.
This bug was active in older Chrome versions (up to around Chrome 81) and Firefox versions (up to Firefox 75). Current versions of both browsers have largely fixed the bug, but the history shows that using display: contents on interactive elements always carries some risk. The current recommendation from the Web Accessibility Initiative (WAI) and the HTML standard is that display: contents should never be used on elements that carry their own semantics in the accessibility tree, meaning not on <button>, <a>, <summary>, <input>, <select>, table elements (<table>, <tr>, <td>, <th>), <fieldset>, <legend>, or list elements (<ul>, <ol>, <li>).
/* display: contents: safe vs. unsafe patterns */
/* SAFE: non-semantic wrapper div, no accessibility role */
.layout-wrapper {
display: contents;
}
/* SAFE: span used as grouping wrapper, no interactive role */
.text-group {
display: contents;
}
/* UNSAFE: button loses its accessible role in older browsers */
/* Do NOT use display: contents on interactive elements */
/*
button.transparent-wrapper {
display: contents; -- NEVER, breaks keyboard navigation + screenreader
}
*/
/* UNSAFE: anchor link loses its href/role */
/*
a.layout-link {
display: contents; -- NEVER, loses focusability and semantics
}
*/
/* UNSAFE: table semantics are lost */
/*
tr.row-group {
display: contents; -- NEVER, browser bugs in table context
}
*/
/* UNSAFE: list structure breaks for screenreaders */
/*
ul.nav-list {
display: contents; -- NEVER, removes list semantics
}
*/
/* ALTERNATIVE: Use CSS Grid subgrid instead of display: contents */
/* for table-like layouts, no accessibility risk */
.grid-row {
display: grid;
grid-column: 1 / -1; /* span full parent grid width */
grid-template-columns: subgrid; /* inherit parent column tracks */
}
5. ARIA and display: contents: what the spec says
The CSS Display Specification (Level 3) makes it clear: display: contents is only meant to remove the element from the visual formatting model, not from the accessibility tree. That means semantic roles, aria-* attributes, and the element's native semantics should be preserved. The specification was clear; browser implementations were, at times, not. This conflict between specification and real world browser implementation is at the core of the accessibility problem.
In practice this means: even if current browsers implement display: contents correctly on semantic elements, caution is warranted. As soon as a user on an older browser lands on a page with display: contents on a <button>, that button might not be usable. Since the goal of accessibility compliance is to include all users, regardless of browser or assistive technology, the rule stands: interactive and semantic elements do not get display: contents. For purely structural wrapper div elements without their own semantics, display: contents is safe to use in production as well.
6. Browser bugs and known incompatibilities
Besides the accessibility bug, there are other known display: contents limitations. Safari only supported display: contents starting with Safari 11.1 and had various layout bugs in combination with flexbox. One known Safari bug: flex children of an element with display: contents did not correctly inherit the flex shorthand properties set on an ancestor. This bug was fixed in later Safari versions, but projects that needed to support Safari 10 had to plan for fallbacks.
Another known bug involves display: contents on table elements: in Chrome and Firefox, table semantics were destroyed by display: contents on a <tr> element, the cells were no longer recognized as belonging to the table, which affected both layout and accessibility. The problem with tables is more fundamental: table elements have explicit rules in the CSS standard for how they are handled when their display value changes, and display: contents does not always interact correctly with those rules. display: contents should generally not be used for table structures.
7. Safe use cases for display: contents
display: contents is safe to use on purely structural, non-interactive elements without native semantics. This typically applies to <div> and <span> elements that only exist as wrappers for JavaScript or framework reasons. The concrete criterion: does the element have its own role in the accessibility tree? If not, no role attribute, no semantic HTML element, no interactive state, then display: contents is safe. If it does, it is not safe.
Three safe use cases in practice: first, framework generated wrappers in data lists (React fragments are an alternative, but sometimes a DOM element is actually needed). Second, CSS Grid subgroups, where semantically grouped elements need to stay in the DOM but should still participate in the parent grid. Third, flex wrappers in navigation, where JavaScript event delegation is needed on a group wrapper that has no semantics of its own. In all three cases the rule holds: the element carrying display: contents must be a <div> or <span> without its own semantics.
/* CSS Grid subgrid: the modern alternative to display: contents */
/* Parent grid */
.products-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto; /* image | content | footer */
gap: 1.5rem;
align-items: start;
}
/* Product card uses subgrid to align across cards */
.product-card {
display: grid;
/* subgrid inherits parent column tracks */
grid-row: span 3; /* occupy 3 row slots */
grid-template-rows: subgrid; /* align content with sibling cards */
background: var(--color-surface-raised, #f8fafc);
border: 1px solid var(--color-border, #e2e8f0);
border-radius: 1rem;
overflow: hidden;
}
.product-card__image { /* row 1, aligned across all cards */ }
.product-card__body { /* row 2, aligned across all cards */ }
.product-card__footer { /* row 3, aligned across all cards */ }
/* display: contents alternative: only for non-semantic wrappers */
.product-group-wrapper {
display: contents; /* transparent: children become direct grid items */
/* Safe because: no semantic role, no interactivity, no ARIA */
}
8. Alternatives: subgrid and other approaches
The cleanest alternative to display: contents for grid layouts is CSS Grid Subgrid. With grid-template-columns: subgrid or grid-template-rows: subgrid, a grid item inherits the tracks of the parent grid. The wrapper element remains a standalone box with full CSS access, background color, border, padding, and still participates in the parent's grid tracks. This solves the original problem (alignment across grid levels) without the accessibility risks of display: contents.
Browser support for subgrid: Chrome 117+, Firefox 71+, and Safari 16+. For projects that need to support older browsers, subgrid is not always an option. Other alternatives depending on the use case: for flexbox, flex-wrap: wrap on the wrapper combined with flex: inherit on the children can achieve similar results. For simple alignment problems, restructuring the markup, using less nesting through CSS Grid named areas, often solves the problem more elegantly than display: contents. The rule of thumb: if subgrid is available, prefer subgrid. If display: contents is needed, first validate that the element carries no semantics.
9. display: contents in direct comparison
The choice between display: contents, CSS Subgrid, and markup refactoring depends on browser support, accessibility requirements, and how much control you have over the generated HTML. For modern projects without legacy browser requirements, subgrid is the better choice in most cases.
| Approach | Accessibility | Browser Support | Use Case |
|---|---|---|---|
| display: contents | Risky on semantic elements | Chrome 65+, FF 37+, Safari 11.1+ | Only div/span without semantics |
| CSS Subgrid | Safe, box is preserved | Chrome 117+, FF 71+, Safari 16+ | Card alignment across rows |
| Markup refactoring | Safe, correct DOM structure | All browsers | When HTML structure is controllable |
| CSS Named Grid Areas | Safe, no DOM change | Chrome 57+, FF 52+, Safari 10.1+ | Template based layout |
| display: contents on button/a | Never, semantics are lost | Bugs in older versions | Forbidden |
In practice: display: contents is a useful tool for narrowly defined use cases, not a universal layout tool. In frameworks like React or Vue that control the DOM structure, it is often better to let the framework generate a flatter structure than to use display: contents as a compromise. For framework independent code where the HTML structure is semantically justified and not easy to change, display: contents on structural <div> elements remains the most pragmatic solution.
Mironsoft
CSS Layout Architecture, Accessibility, and Hyva Theme Development
Solve layout problems without accessibility risks?
We analyze your CSS layouts, identify display: contents pitfalls, and implement subgrid based solutions that are both technically sound and accessibility compliant.
Layout Audit
Grid and flexbox layout analysis, display: contents risk check, and subgrid migration
Accessibility Review
ARIA correctness, screen reader testing, and WCAG compliance review for CSS layouts
Hyva Layouts
Magento 2 product lists and category layouts with subgrid and Tailwind CSS
10. Summary
display: contents removes an element from the visual layout model but lets its children participate directly in the grid or flexbox context of the nearest real ancestor. This is useful for structural wrapper div elements that exist in the DOM only for JavaScript or framework reasons and should not interfere with the layout. The critical restriction: display: contents must never be used on interactive or semantic elements, not on <button>, <a>, <input>, not on table elements, not on list elements. The known accessibility bugs in Chrome and Firefox have shown in the past what happens when this rule is broken.
For most modern projects, CSS Subgrid is the better alternative: the wrapper element remains its own box but inherits the grid tracks of the parent container. This solves the alignment problem without accessibility risks. display: contents remains a valid tool for a narrowly defined use case, structural, non-semantic wrapper elements in grid or flexbox, but it is not a universal layout tool and should be used with caution. Browser compatibility has been solid across all modern browsers since 2022; the accessibility question remains the deciding factor for safe usage.
CSS display: contents: The Essentials at a Glance
What it does
Removes the element's box from the layout. Children become direct items of the nearest real layout context (Grid/Flex).
Never use on
button, a, input, select, table/tr/td/th, ul/ol/li, fieldset/legend. Risk of accessibility bugs and semantic loss.
Safe usage
Only on div and span without their own semantics, used as structural wrappers with no layout function in the DOM.
Modern alternative
CSS Subgrid (grid-template-columns/rows: subgrid), solves the same problem without accessibility risks. Chrome 117+, FF 71+, Safari 16+.