Display modes and the hide decision tree
The CSS display property and how it governs whether an element generates a box, what layout rules it imposes on its children, and the three distinct ways to hide it.
Wrap a piece of text in a <span>, give it w-40 pt-4, and nothing happens. The width is ignored, the top padding does something strange you can’t quite pin down, and the text just sits there at its natural size. Move the exact same content into a <div> with the exact same two classes and both snap into effect: the box is 160 pixels wide with 16 pixels of space on top. Same content, same CSS, opposite results. The only thing that changed is the element, and the only thing that matters about the element here is one property it carries by default: display.
In the last lesson you learned how padding and width compose: how the four boxes nest and add up under border-box. This lesson is about whether they apply at all. display is the single property that decides which box-model rules an element honours, which sizing utilities do anything, and how the element sits relative to its neighbours. It is the master switch for how an element takes part in layout, and most layout confusion traces back to it. We’ll cover it in two passes. First come the handful of modes an experienced developer actually reaches for, and the question each one answers; then the three different ways to hide an element, which turns out to be a sharper decision than it first looks.
Display sets two things: an outer role and an inner formatting context
Section titled “Display sets two things: an outer role and an inner formatting context”Before the catalogue of modes, it helps to have the model that makes every one of them easy to read. The trap with display is treating it as a flat list of eight magic keywords to memorise. It isn’t a list. Every display value answers two questions at once, and once you see those two questions, the keywords fall into a small grid you can reason about rather than recall.
The first question is outer: how does this element behave toward its siblings? Two answers matter here. A block-level box takes its own line and fills the width available to it, so if you stack two of them the second sits below the first. An inline-level box flows along inside a line of text, sitting between words and wrapping with them, like the bold or linked words in this very sentence.
The second question is inner: what layout rules does this element impose on its own children? This is the element’s formatting context . The default is normal flow , where children stack as blocks or flow as inline. But display can switch that inner context to flex (children become a row or column) or grid (children fall into a two-dimensional grid).
So block and inline are pure outer choices that leave the inner context as normal flow. flex and grid change the inner context while leaving the outer role at its block-level default. Once you see the two axes as independent, a question that confuses every newcomer answers itself: what’s the difference between flex and inline-flex? They share the same inner context, since both lay their children out as a flex row or column. They differ only in their outer role: flex is block-level and takes its own line, while inline-flex is inline-level and flows in text. inline-flex is just the inline-outer cell of the same grid, not a separate keyword to memorise.
You’ll occasionally meet a newer two-value form of the property in modern CSS, like display: block flow or display: inline flex, which writes the outer and inner parts out explicitly as two words. That syntax exists precisely to make the split in the grid above visible in code. You don’t need to write it; the single-keyword forms like flex and inline-flex are what you’ll type. It’s worth recognising on sight, because it gives the two-axis model a name in the spec.
Block, inline, and inline-block in normal flow
Section titled “Block, inline, and inline-block in normal flow”Three of those keywords you already half-know, because they’re the default display values of the HTML elements you’ve been writing since the JSX lesson. What you may not have spelled out is the layout consequence of each, which is exactly what the opening puzzle turned on.
block is the workhorse default. A block box starts on a new line, fills its parent’s inline axis, and honours every box-model property you throw at it: width, height, every margin and padding. This is the default for <div>, <p>, headings, the sectioning and landmark elements, lists, and <form>. It’s why the <div> in the opening honoured w-40 pt-4 without complaint, since block boxes accept all of it.
inline behaves in the opposite way. An inline box flows inside the text line, sitting between words like a <span>, <a>, <strong>, or <em>. The catch is this: an inline box ignores width and height entirely, and while vertical padding and margin technically render, they don’t push the surrounding lines apart. The padding paints over the lines above and below instead of reserving its own vertical space. That’s the full explanation for the opening puzzle. The <span> ignored w-40 because inline boxes don’t take a width, and pt-4 did that “something strange” because inline vertical padding paints without reserving space. The element wasn’t broken; it was behaving exactly as an inline box must.
inline-block is the deliberate bridge between the two. It flows in the text line like inline, sitting mid-sentence and wrapping with the words, but it accepts width and height and reserves its vertical space like block. It’s the “I want this thing in the text line but I also want to size it” mode.
The fastest way to internalise this is to flip an element between the three modes and watch what changes. In the playground below, a small box sits inside a real line of flowing text. Switch its display and watch two things at once: whether the width you set takes effect, and whether the box pushes the lines above and below it apart or quietly overlaps them.
Your plan renews monthly, and the sized box sits right here in the sentence, so you can see how it flows against the lines above and below it.
Watch the width “switch on” the moment you leave inline: that is the behaviour this section is about.
So when do you reach for inline-block in 2026? Rarely, and only for genuinely in-text things: a status dot, a small badge, or a count that needs a width while sitting mid-sentence. One redirect saves you most of the times you’d be tempted. When you catch yourself thinking “I want to put a width on this span,” what you almost always actually want is for the parent to be a flex container and this element to be a flex item. So reach for flex on the parent, not inline-block on the child. Sizing a child by switching its own display is the rare case; arranging children with a flex parent is the common one, and keeping that instinct in mind heads off the most common misuse of inline-block.
Flex and grid are container modes
Section titled “Flex and grid are container modes”The two workhorses of modern layout are flex and grid, and both are inner-context switches: they change how an element lays out its children.
flex makes a one-dimensional container, a single row or a single column, and turns its direct children into flex items that line up along that axis. grid makes a two-dimensional container, rows and columns at once, and its children fall into grid cells. That’s the entire distinction you need today: one axis versus two. The algorithms that make them powerful (how items grow and shrink, how you define columns, how you align everything) get a full lesson each later in this chapter. Here we’re only naming the modes so you can pick one; the mechanics come next.
Their inline twins, inline-flex and inline-grid, give children the same flex or grid layout while the container itself flows inline among other content. The case you’ll write most often is an icon-and-text button. The button needs flex to align its icon and label, but the button itself should sit inline next to other content rather than claiming its own line:
<button className="inline-flex items-center gap-2"> <PlusIcon /> New invoice</button>The inline-flex is the part to focus on here; items-center and gap-2 are flexbox alignment utilities that get explained properly in the flexbox lesson later in this chapter, shown now only to make the inline-flex case concrete.
Hold on to one forward-looking default as you go through the rest of this chapter: flex for a row or a column, grid for a two-dimensional structure. Most real SaaS interfaces are a grid of flex compositions, with a grid laying out the page regions and flex arranging the contents inside each. You don’t have the tools to build that yet, but settling on the heuristic now means the next two lessons feel like filling in how, rather than learning what.
display: contents, the layout-tree unwrapper
Section titled “display: contents, the layout-tree unwrapper”contents is the one display value that genuinely surprises people, so it’s worth slowing down for. Every other mode generates some kind of box. contents generates none: it removes the element’s own box from the layout tree entirely, while leaving its children in place. The children then take part in the grandparent’s layout exactly as if the wrapper weren’t there. The element still exists in the DOM, holding its attributes and its meaning, but as far as layout is concerned it’s transparent.
There’s a specific, common reason you’d want that. Picture a flex row of three cards, except the three cards are grouped inside a <section> for semantic reasons. Now the <section> is the single child of the flex container, so the flex container has exactly one flex item, the section, and the three cards stack inside it instead of spreading across the row. Your semantic wrapper has broken your layout. You have two ways out: delete the wrapper and lose the meaning, or add display: contents to it. contents dissolves the section’s box so the three cards become the flex items, while the <section> stays in the DOM for structure and accessibility. The other common case is component composition, where a component renders a real DOM element it needs for some reason, but you don’t want that element’s box interfering with the layout around it.
The before-and-after is the clearest way to see the mechanic. The two tabs below are the same markup; the second adds one class to the wrapper.
<div className="flex gap-4"> <section> <article className="card">Starter</article> <article className="card">Pro</article> <article className="card">Scale</article> </section></div>The section is the only flex item. The flex container sees one child, the <section>, so the three cards stack vertically inside it instead of spreading across the row. The wrapper’s box is in the way.
<div className="flex gap-4"> <section className="contents"> <article className="card">Starter</article> <article className="card">Pro</article> <article className="card">Scale</article> </section></div>The cards are the flex items now. contents removes the section’s box from the layout tree, so its three children promote up and become the flex container’s direct items, spreading across the row. The <section> still exists in the DOM.
That promotion is the mechanic to keep in your head, and it’s the easy one to forget when you’re scanning a tree in DevTools: contents makes the children the flex or grid items, not the wrapper. The wrapper itself is gone from layout.
There’s an important caveat, because contents has a history. For years, putting display: contents on a semantic element didn’t just remove its box, it removed the element from the accessibility tree too, so a display: contents <ul> stopped being announced as a list to screen readers. Modern browsers have fixed this for most semantic elements, but the fix isn’t universal, and the CSS spec still flags the old behaviour as a bug that some engines carry. So the rule to follow is this:
That ordering, Fragment first and contents only when a DOM element is mandatory, is the habit to build. You already know Fragments from the JSX lesson; contents is the tool for the narrower case where the node has to stay.
The Tailwind display utilities
Section titled “The Tailwind display utilities”Every mode you’ve met maps one-to-one to a Tailwind utility you type directly. Here’s the complete set, so the catalogue is concrete:
| Utility | display value |
| --- | --- |
| block | block |
| inline-block | inline-block |
| inline | inline |
| flex | flex |
| inline-flex | inline-flex |
| grid | grid |
| inline-grid | inline-grid |
| contents | contents |
| hidden | none |
A few more are worth recognising but not memorising in detail. flow-root establishes a fresh block formatting context, a niche tool for containing floats and stray margins. table, table-row, and table-cell cover the rare case where you need table-style alignment on non-tabular markup; real data tables use <table> elements, covered with the HTML tables lesson. Know they exist, and reach for them almost never.
Two things are worth pinning down here, because both surprise people who expect the framework to have smoothed them over.
The first: Preflight does not normalise display. Recall Preflight from the styling chapter, the reset that zeroed your margins and set border-box everywhere. It deliberately leaves display defaults alone. A <div> is still block, and a <button> still carries its quirky default display that varies slightly across browsers. That’s the real reason you write inline-flex items-center gap-2 explicitly on an icon-and-text button rather than trusting it to align its icon and label on its own: you’re not fixing Tailwind, you’re overriding a browser default Preflight chose not to touch.
The second: Tailwind’s hidden is the same thing as the HTML hidden attribute. Both resolve to display: none. That’s the bridge into the rest of the lesson, because display: none is the first of three quite different ways to make an element disappear.
Three ways to hide, two trees to hide from
Section titled “Three ways to hide, two trees to hide from”This is the part the lesson is named for. “Hide this element” sounds like one operation, but it’s really three, and which one you want depends on a question most people never think to ask: hide it from what?
There’s more than one answer because the browser doesn’t keep one model of your page, it keeps two. The first is the layout tree you already met: the tree of boxes that decides what gets painted on screen and what takes up space. The second is the accessibility tree , a parallel structure the browser hands to screen readers and other assistive technology , which decides what gets announced to someone who isn’t looking at the screen. An element can be present or absent in each tree independently, and that independence is the whole idea here. “Hide from sight” and “hide from a screen reader” are different operations, so conflating them is how you ship a page that looks fine but is quietly broken for assistive-tech users.
The two trees give you a 2×2. Place each hiding tool in its cell and the ambiguity dissolves.
sr-only aria-hidden="true" on screen, takes space, silent to screen readers — e.g. a decorative icon display: none / hidden no box, no space, not announced — also what conditional render produces visibility: hidden The odd one out — it doesn't fit the grid. Absent from the accessibility tree and paints nothing, but still reserves its box in the layout tree. So it's gone from sight and from screen readers, yet still holding its space. Here are the three tools, each described by its effect on both trees.
display: none (Tailwind hidden, or, far more often in React, simply not rendering the element) removes it from both trees at once. No box, no space on the page, nothing announced to a screen reader. It’s the complete disappearance, and it’s the right tool when the content is genuinely not present right now: a closed dialog, the inactive panel of a tab group, a route that isn’t mounted. In React the idiomatic form of “not present right now” usually isn’t hidden at all; it’s conditional rendering:
{isOpen && <SettingsPanel />}When isOpen is false the element never enters the DOM, so it’s absent from both trees just like display: none, with the bonus that React also tears down its state. This is the same && pattern from the JSX lesson, and the same reason to keep the left side a real boolean: write isOpen && …, never a raw count or a possibly-null value on the left, or you’ll render a stray 0. Treat conditional render and display: none as two doors to the same room. Reach for conditional render when the element’s existence tracks React state, and hidden when you’re toggling a class.
visibility: hidden (Tailwind invisible) is the strange middle case. It keeps the element’s box and its reserved space in the layout tree, so the element still pushes its neighbours around exactly as if it were visible, but it paints nothing and drops out of the accessibility tree. You get an invisible element that still takes up its space. That sounds useful, but it’s almost always the wrong tool. When you catch yourself reaching for invisible to “reserve the space” for something, the real problem is usually a sizing one: you want the container to hold a fixed size regardless of its contents, which is a job for grid, min-h-*, or aspect-ratio (sizing gets its own lesson later in this chapter). Learn to recognise invisible so you know what it does when you see it, reach for it rarely, and treat your own urge to use it as a signal to reconsider the layout underneath.
aria-hidden="true" is the mirror image of visibility: hidden. The element stays fully visible and takes its space, untouched on screen, but it’s pruned from the accessibility tree, so a screen reader skips it entirely. The canonical case is a decorative icon sitting next to a redundant text label:
<button className="inline-flex items-center gap-2"> <TrashIcon aria-hidden="true" /> Delete</button>The word “Delete” is already there for the screen reader; the icon is pure decoration. Without aria-hidden a screen reader might announce something like “image, Delete,” reading the icon as noise before the real label. Contrast this with the icon-only button from the HTML semantics chapter: there, the icon was the only content, so it earned an aria-label to give it a name. Here there’s already a visible label, so the icon gets aria-hidden to remove it from the announcement. Same icon, opposite treatment, decided entirely by whether a text label is already present.
Put the three together and the decision is a short tree. Ask the questions in order, and the order matters: asking “is it even present right now?” first eliminates two of the three tools before you ever weigh them. Walk through it below.
Don’t render it. {isOpen && <Panel />} keeps it out of the DOM, absent from both the layout and accessibility trees, and tears down its React state too. Reach for the hidden class instead only when you’re toggling a class rather than mounting and unmounting.
Tailwind’s sr-only hides the element visually while keeping it in the accessibility tree, so screen readers still announce it. This is the right tool here; its mechanics and the full visually-hidden discipline get a dedicated treatment in the accessibility chapter later in the course.
aria-hidden="true" keeps the element painted and taking space, and removes it from the accessibility tree only. This is the case for a decorative icon sitting next to a label. Never on a focusable or interactive element.
visibility: hidden keeps the box and its space while painting nothing. It works, but wanting it is usually a sign the real fix is a sizing one: a fixed-size container via grid, min-h-*, or aspect-ratio. Use it knowingly and rarely.
Notice the shape of the walk: the first question is never “which class do I type,” it’s “is this even present?” Most “how do I hide this” questions are really “should this exist right now,” and answering that first sends you straight to conditional render without ever touching aria-hidden or invisible.
Recall check: name the mode, choose the way to hide
Section titled “Recall check: name the mode, choose the way to hide”Two quick checks before you move on, one for each half of the lesson. First, the modes. Each scenario below is a piece of real UI; drag it into the display mode you’d reach for. The aim is to name the mode before you write a class, which is exactly what this checks.
Drag each piece of UI into the display mode you'd reach for it. Drag each item into the bucket it belongs to, then press Check.
<section> wrapper that shouldn’t break its parent’s flex rowNow the hiding decision, in code. The starter has two problems. First, a “Delete” button pairs a decorative icon with a visible label, but the icon isn’t hidden from screen readers, so it gets announced as noise. Second, a debug panel renders unconditionally, when it should only be in the DOM while showDebug is true. Fix both: hide the icon from assistive tech without making it invisible on screen, and make the panel genuinely absent when the flag is off.
Hide the decorative trash icon from screen readers (it sits next to a visible 'Delete' label), and render the debug panel only when showDebug is true.
Reference solution
export function App() { const showDebug = false;
return ( <div> <button className="inline-flex items-center gap-2"> <span role="img" aria-hidden="true">🗑️</span> Delete </button> {showDebug && <div className="debug-panel">Secret debug info</div>} </div> );}The icon gets aria-hidden="true", since it’s decoration sitting next to a real label, so it should be silent. The panel moves behind showDebug && …, so when the flag is off the element never enters the DOM. Note what we didn’t do: no aria-hidden on the button itself (that would strand a keyboard user on a silent control), and no invisible or hidden on the panel (it shouldn’t reserve space or linger in the DOM; it should be genuinely gone).
External resources
Section titled “External resources”Below are two canonical references for the modes, one interactive primer, and the accessibility-tree side that the hide decision tree turned on, which gets its full treatment later in the course.
The outer/inner two-value model and the full keyword set, with live examples for every value.
The unwrapper mode and the accessibility caveat, with the current state of browser support.
An interactive primer with live toggles for how inline, block, and inline-block honour sizing and spacing.
How aria-hidden prunes the accessibility tree, the focusable-element warning, and the decorative-icon pattern.