Skip to content
Chapter 20Lesson 2

Display modes and the hide decision tree

The CSS display property and the three ways to hide an element.

Wrap a piece of text in a <span>, give it w-40 pt-4, and nothing happens: the width is ignored, the top padding behaves oddly, and the text sits at its natural size. Move the same content into a <div> with the same two classes and both snap into effect: the box becomes 160 pixels wide with 16 pixels of space on top. Same content, same CSS, opposite results. The only difference is the element, and the only thing about the element that matters here is one property it carries by default: display.

The last lesson covered 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 decides which box-model rules an element honors, which sizing utilities do anything, and how the element sits relative to its neighbors. We cover it in two passes: first the handful of modes you actually reach for and the question each one answers, then the three ways to hide an element.

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”

display looks like eight magic keywords to memorize, but every value answers the same two questions, and the answers fall into a small grid you can reason about.

The first question is outer: how does this element behave toward its siblings? A block-level box takes its own line and fills the available width, so stack two and the second sits below the first. An inline-level box flows inside a line of text, sitting between words and wrapping with them, like the bold or linked words in this 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; display can switch it to flex (children become a row or column) or grid (children fall into a two-dimensional grid).

So block and inline set only the outer role and leave the inner context as normal flow, while flex and grid change the inner context and leave the outer role block-level. With the two axes separated, the question that confuses every newcomer answers itself: flex and inline-flex share the same inner context — both lay children out as a flex row or column — and differ only in outer role, flex taking its own line and inline-flex flowing in text. inline-flex is just the inline-outer cell of the grid, not a separate keyword.

inner → outer ↓
flow (normal)
flex
grid
block-level own line
block
flex
grid
inline-level flows in text
inline
inline-flex
inline-grid
The six core display keywords as one grid: outer role (toward siblings) down, inner formatting context (for children) across.

Modern CSS also offers a two-value form, like display: block flow or display: inline flex, that writes the outer and inner parts as two explicit words; you’ll keep typing the single keywords, but it’s worth recognizing on sight.

Block, inline, and inline-block in normal flow

Section titled “Block, inline, and inline-block in normal flow”

You already half-know these three keywords: 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 each one’s layout consequence, which is exactly what the opening puzzle turned on.

block is the default for <div>, <p>, headings, lists, <form>, and the sectioning elements. A block box starts on a new line, fills its parent’s inline axis, and honors every box-model property: width, height, and every margin and padding. That is why the <div> in the opening honored w-40 pt-4.

inline is the default for text-level elements like <span>, <a>, <strong>, and <em>. An inline box flows inside the text line, sitting between words, and it ignores width and height entirely. Vertical padding and margin still render, but they don’t push the surrounding lines apart; the padding paints over the lines above and below instead of reserving its own space. That resolves the opening puzzle: the <span> ignored w-40 because inline boxes take no width, and pt-4 painted over the neighboring lines because inline vertical padding reserves none.

inline-block bridges the two. It flows in the text line like inline, wrapping with the words, but it accepts width and height and reserves its vertical space like block.

To internalize this, flip one element between the three modes and watch two things: whether the width takes effect, and whether the box pushes the surrounding lines apart or quietly overlaps them.

Flip the box's display and watch the readouts: 'inline' ignores the width and reserves no vertical space, 'inline-block' takes the width and reserves space while staying in the line, 'block' takes the width but breaks onto its own line.

So when do you reach for inline-block in 2026? Rarely, and only for genuinely in-text things that need a width: a status dot, a small badge, a count sitting mid-sentence. When you catch yourself wanting to put a width on a span, what you usually want is a flex parent with this element as a flex item, so reach for flex on the parent instead.

flex and grid are the two main layout modes, and both are inner-context switches: they change how an element arranges its children.

flex makes a one-dimensional container, a single row or a single column, and lines its direct children up along that axis. grid makes a two-dimensional container, rows and columns at once, and places its children into cells. One axis versus two is the whole distinction; how items grow, how you define columns, and how you align everything get a dedicated lesson each next.

Their inline twins, inline-flex and inline-grid, lay out children the same way while the container itself flows inline among other content. The case you’ll write most often is an icon-and-text button: it needs flex to align its icon and label, but should sit inline beside other content rather than claim its own line.

<button className="inline-flex items-center gap-2">
<PlusIcon />
New invoice
</button>

Focus on inline-flex here; items-center and gap-2 are alignment utilities covered in the flexbox lesson.

Carry one default into the rest of the chapter: flex for a row or a column, grid for a two-dimensional structure. Most SaaS interfaces are a grid of flex compositions, a grid laying out the page regions and flex arranging the contents inside each.

display: contents: promote children into the grandparent’s layout

Section titled “display: contents: promote children into the grandparent’s layout”

Every display mode generates a box except one. contents generates none: it removes the element’s own box from the layout tree , and its children take its place in the grandparent’s layout as if the wrapper weren’t there. The element stays in the DOM with its attributes and meaning; only its box is gone.

The common reason to want this is a semantic wrapper that breaks a layout. Picture a flex row of three cards wrapped in a <section>. The flex container now has one item, the section, so the cards stack inside it instead of spreading across the row. You can delete the wrapper and lose the meaning, or set display: contents on it so the three cards become the flex items while the <section> stays in the DOM.

<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. Its box sits between the container and the cards, so the three cards stack inside it instead of spreading across the row.

The mechanic to remember, and the one that surprises you in DevTools: contents makes the children the flex or grid items, not the wrapper.

There’s one caveat. display: contents once removed elements from the accessibility tree as well, so a display: contents <ul> stopped being announced as a list. Modern browsers have fixed this for most semantic elements, but not all, so reach for a Fragment first.

Every mode you’ve met maps one-to-one to a Tailwind utility. Here’s the complete set:

Utilitydisplay value
blockblock
inline-blockinline-block
inlineinline
flexflex
inline-flexinline-flex
gridgrid
inline-gridinline-grid
contentscontents
hiddennone

A few more exist for niche cases — flow-root for containing floats, and table, table-row, and table-cell for table-style alignment on non-tabular markup — but you’ll reach for them almost never.

Two facts surprise people who expect the framework to have smoothed them over.

First, Preflight does not normalize display. Preflight leaves every element’s default display alone: a <div> stays block, and a <button> keeps its quirky default that varies slightly across browsers. That’s why you write inline-flex items-center gap-2 explicitly on an icon-and-text button. You’re not fixing Tailwind, you’re overriding a browser default Preflight chose not to touch.

Second, Tailwind’s hidden is the HTML hidden attribute: both resolve to display: none. That’s the bridge into the rest of the lesson, since display: none is the first of three 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”

“Hide this element” sounds like one operation, but it’s three, and which one you want turns on a question most people never ask: hide it from what?

There’s more than one answer because the browser keeps two models of your page, not one. The first is the layout tree you already met: the boxes that decide what gets painted and what takes up space. The second is the accessibility tree , which the browser hands to screen readers and other assistive technology to decide what gets announced to someone who isn’t looking. An element can be present or absent in each tree independently, and that independence is the whole idea. “Hide from sight” and “hide from a screen reader” are different operations, so conflating them ships a page that looks fine but is quietly broken for assistive-tech users.

The two trees give you a 2×2. Drop each tool into its cell and the ambiguity dissolves.

in layout tree? → in a11y tree? ↓
Yes painted · takes space
No no box
Yes announced
Visible & announced the default — a normal element
(rarely useful alone) visually gone yet still announced is the job of sr-only
No silent
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.
Two trees, four corners. 'Hide' is ambiguous until you say which tree you mean. visibility: hidden is the odd one out — gone from sight and from screen readers, yet still holding its space.

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, nothing announced. It’s the complete disappearance, right when the content is genuinely not present: a closed dialog, the inactive panel of a tab group, a route that isn’t mounted. In React that 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, and React tears down its state too. This is the && pattern from the JSX lesson, with the same reason to keep the left side a real boolean: write isOpen && …, never a raw count or a possibly-null value, or you’ll render a stray 0. 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 reserved space in the layout tree, so the element still pushes its neighbors around as if visible, yet paints nothing and drops out of the accessibility tree. You almost never want it. When you reach for invisible to “reserve the space” for something, the real problem is usually sizing: you want the container to hold a fixed size regardless of its contents, which is a job for grid, min-h-*, or aspect-ratio (a later lesson in this chapter). Recognize invisible so you know what it does when you see it, and treat the urge to use it as a signal to fix the layout underneath.

aria-hidden="true" is the mirror image. 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. 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 “image, Delete,” reading the icon as noise before the real label. Contrast 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 a visible label already exists, so the icon gets aria-hidden to remove it from the announcement. Same icon, opposite treatment, decided by whether a text label is already present.

Put the three together and the decision is a short tree, and the order matters: asking “is it even present right now?” first eliminates two of the three tools before you weigh them.

Which way should I hide it?

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.

Check your understanding: name the mode, then hide an element

Section titled “Check your understanding: name the mode, then hide an element”

First, the modes. Name the mode you’d reach for before writing a single class.

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.

block Own line, stacks vertically
inline Flows in the text line
inline-flex Flex layout, flows in text
grid Two-dimensional structure
contents Wrapper box removed from layout
A paragraph of body copy
A keyword highlighted mid-sentence
A button with an icon and a text label
A responsive card gallery laid out in rows and columns
A semantic <section> wrapper that shouldn’t break its parent’s flex row

Now the hiding decision, in code. The starter has two problems. A “Delete” button pairs a decorative icon with a visible label, but the icon isn’t hidden from screen readers, so it’s announced as noise. And a debug panel renders unconditionally, when it should be in the DOM only while showDebug is true.

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.

Preview
    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" because it’s decoration beside a real label, so it should be silent. The panel moves behind showDebug && …, so when the flag is off it never enters the DOM. Note what we didn’t do: no aria-hidden on the button itself, which would strand a keyboard user on a silent control, and no invisible or hidden on the panel, which would reserve space or leave it lingering in the DOM when it should be gone.