CSS Display Contents
The display property decides how an element participates in layout: does it make a box, does it flow
inline, does it become a flex/grid container, etc.
And then there’s display: contents, which is basically: “pretend this element’s box
does not exist… but keep its children.”
That sounds magical (and sometimes it is), but it also comes with important tradeoffs—especially for background/border/padding and accessibility.
CSS display contents property
When you set display: contents on an element:
- The element does not generate its own box in the layout.
- Its children behave as if they were direct children of the element’s parent.
- The element’s own background, border, and padding won’t render, because there is no box to paint them on.
Think of it as “remove the wrapper box” without removing the wrapper node from the DOM.
First intuition: a wrapper that doesn’t wrap
.wrapper {
display: block;
}
.wrapper {
display: contents;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: ui-sans-serif, system-ui, sans-serif;
padding: 16px;
}
.outer {
border: 3px solid #111;
padding: 12px;
display: flex;
gap: 12px;
align-items: center;
}
.wrapper {
border: 3px dashed #111;
padding: 12px;
}
.badge {
display: inline-block;
border: 2px solid #111;
padding: 6px 10px;
font-weight: 700;
}
.note {
opacity: 0.85;
}
Child A Child B Child CToggle the snippet. With contents, the dashed wrapper box disappears.
In the first snippet, the wrapper has a visible dashed border and padding, because it creates a box. In the second snippet, the wrapper creates no box, so the dashed border and padding vanish—yet the children still render and sit inside the flex layout.
CSS display contents example
The most common “wow” use-case is when a wrapper element breaks a layout, especially in CSS Grid.
A classic example: you want a two-column form grid where label is column 1 and input is
column 2. But your HTML groups each field inside a wrapper like <div class="field">. That wrapper
becomes the grid item, so the label/input can’t align across rows.
Example: grid form alignment with display: contents
.field {
display: block;
}
.field {
display: contents;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: ui-sans-serif, system-ui, sans-serif;
padding: 16px;
}
.form {
border: 3px solid #111;
padding: 16px;
display: grid;
grid-template-columns: 140px 1fr;
gap: 10px 12px;
max-width: 560px;
}
label {
font-weight: 700;
align-self: center;
}
input,
select {
border: 2px solid #111;
padding: 10px 12px;
font: inherit;
width: 100%;
}
.help {
grid-column: 1 / -1;
opacity: 0.85;
margin-top: 6px;
}
What’s happening:
-
With
.field { display: block; }, each.fieldis a single grid item, so the grid sees three items (one per row), and the label/input can’t become separate columns. -
With
.field { display: contents; },.fieldstops generating a box, so its children (labelandinput/select) become the grid items. Now the grid can place each label in column 1 and each control in column 2 automatically. -
It's a bit like CSS Subgrid, but without needing to make the inner element a grid container. The children just
step up and become part of the parent grid.
Also this
display: contentsmethod has ~96% browser support, while subgrid is at ~88%.
CSS display contents background color / CSS display contents border
This is the part that surprises people (usually the hard way):
- If the element doesn’t generate a box, it has nowhere to paint a background.
- No box also means no border and no padding.
In other words: you can’t “style the wrapper” visually if you remove the wrapper’s box.
Background, border, padding demo
.panel {
display: block;
}
.panel {
display: contents;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo {
font-family: ui-sans-serif, system-ui, sans-serif;
padding: 16px;
}
.shell {
border: 3px solid #111;
padding: 14px;
max-width: 620px;
}
.panel {
background: #eee;
border: 3px dashed #111;
padding: 14px;
}
.panel h4 {
margin: 0 0 8px 0;
}
.pill {
display: inline-block;
border: 2px solid #111;
padding: 6px 10px;
font-weight: 700;
margin-right: 6px;
}
Wrapper styles live here
One Two Three
Notice how the dashed border and gray background vanish when display: contents is active.
The children remain, but the wrapper styling is gone.
So how do I get a border then?
You have a few options:
- Move the border/background/padding to a real box (often a child).
-
Add a “visual wrapper” element inside, and put
display: contentson the outer wrapper only. -
If you only needed spacing, prefer
gapon the parent grid/flex instead of padding on the removed wrapper.
CSS display contents alternatives
display: contents is best when:
- You need the children to participate directly in a parent layout (grid/flex).
- The wrapper is present for structure, grouping, hooks, or scripting—but not for visuals.
If that’s not your situation, these alternatives are often safer (and sometimes simpler):
Alternative 1: change the HTML structure
If you control the markup, the cleanest fix is often: don’t add the wrapper in the first place.
For example, in a form grid you can place label and input as direct children of the grid
container (no .field wrapper).
It’s boring, but boring is stable.
Alternative 2: use grid-template-areas
When wrappers exist for a reason, you can still get aligned layouts using named areas, or by explicitly placing items. This is more CSS, but it avoids removing boxes and avoids accessibility surprises.
Alternative 3: subgrid (when the problem is
nested grids)
Sometimes you’re reaching for display: contents because you want a nested grid to align with the parent
grid.
That’s exactly the job of subgrid (where supported).
Rule of thumb: if your wrapper exists because it’s a component boundary (card, row, field),
subgrid can be a better semantic fit than “erase the wrapper box.”
Learn more about subgrid in the CSS Subgrid Interactive Tutorial.
Alternative 4: flexbox or flow layout
If the only reason you’re using grid is “two columns,” flexbox or regular flow might be enough—especially for simple layouts.
Learn more about flexbox in the CSS Flexbox Interactive Tutorial.
CSS display contents support (browser support)
Browser support for display: contents is broadly good. On Can I use, global support is around
95.83% (so roughly ~96% at the time of writing).
Here’s the Can I use page: https://caniuse.com/css-display-contents
Support is not the whole story, though. The bigger practical concern today is usually accessibility behavior, not “does it render.”
CSS display contents accessibility
Some browser implementations have had (and some still have) cases where an element with
display: contents is treated incorrectly in the accessibility tree.
MDN notes that some browsers remove the element itself from the accessibility tree (while leaving descendants),
which can cause the element to no longer be announced by screen readers—even though that’s not what the spec
intends.
What can go wrong in real life
- A wrapper with semantic meaning (or ARIA) becomes “invisible” to assistive tech.
-
If you put
aria-label,role, or relationships on the wrapper, those may not behave how you expect when the wrapper has no box and is mishandled in the accessibility tree. -
Certain patterns (notably complex table navigation in some screen reader + browser combos) have historically
been buggy when display properties like
contentsare involved.
Safe patterns
If you want a simple safety rule that will keep you out of trouble most of the time:
-
Use
display: contentson neutral wrappers likedivelements that exist only for layout grouping. -
Avoid using it on semantic elements that carry meaning by themselves (for example:
button,fieldset,label,nav,main,section, lists, table structures, etc.). - Avoid using it on elements that carry important ARIA roles/labels/relationships.
Accessibility-friendly form grid pattern
In the form grid demo above, we used display: contents on .field, which is a neutral
wrapper.
That’s typically the “sweet spot” use-case.
The labels and inputs remain native, focus works normally, and you didn’t erase the semantics of the important elements themselves.
A practical testing checklist
- Navigate the UI with only a keyboard (Tab, Shift+Tab, Enter, Space).
- If you can, test with at least one screen reader + browser combo (for example: VoiceOver + Safari on macOS, or NVDA + Firefox on Windows).
- If the wrapper had ARIA (labels/roles), verify those announcements still happen.
-
If anything “goes silent” or navigation becomes weird, remove
display: contentsand use one of the alternatives instead.
Quick recap: when to use display: contents
- Use it when a wrapper breaks grid/flex layout and you need the children to become layout items.
- Don’t expect borders/backgrounds/padding to work on that element (no box, no paint).
- Be cautious with accessibility: keep it on neutral wrappers, and test your UI with keyboard and (ideally) a screen reader.
One last pro tip
If you find yourself adding display: contents everywhere, it might be a sign your HTML structure (or
component boundaries) and your layout strategy are fighting each other.
Sometimes the best CSS trick is a markup refactor.
