Lesson 11 of 25

CSS Display Property

display Answers Two Questions at Once

display is the most consequential property in CSS layout, because it decides two separate things. First, how this element behaves among its siblings — does it take a whole line to itself, or does it sit in the flow of text? Second, how this element arranges its own children — normally, or as a flex or grid layout.

Every HTML element already has a default. div, p, h1, section and li are block by default. span, a, strong, em and img are inline. Nothing stops you changing that — a span set to display: block behaves exactly like a div. But changing display never changes an element's meaning, so a div styled to look like a button is still not a button to a screen reader or to the keyboard. Pick the right HTML element first, then style it.

The values you will actually use are few: block, inline, inline-block, flex, grid and none. Everything else is either rare or a specialist tool.

Example
/* These are defaults you are overriding, not inventing */
div, p, h1, section, li { display: block; }
span, a, strong, em     { display: inline; }

/* Change the behaviour without changing the meaning */
.nav a {
  display: block;      /* the link now fills its line and accepts padding */
  padding: 12px 16px;
}

/* flex and grid change how the CHILDREN are arranged */
.toolbar { display: flex; }
.gallery { display: grid; }
  • block — starts on a new line and fills the available width
  • inline — flows within a line of text and takes only the width it needs
  • inline-block — flows in a line, but accepts width, height and vertical spacing
  • flex / grid — the element itself stays a block, but its children are laid out by that system
  • none — the element is removed from the page entirely
  • Changing display never changes what an element means to assistive technology

What Inline Elements Quietly Refuse to Do

Inline elements have three limitations that are easy to miss, because in each case your CSS is accepted and simply has no effect.

They ignore width and height completely. An inline element is as wide as its content, full stop. Setting width: 200px on a <span> changes nothing at all, and there is no warning.

They also ignore vertical margins, and their vertical padding behaves strangely. Add padding: 20px to a <span> inside a paragraph and the background does grow upwards and downwards — but the surrounding lines do not move, so the padded area overlaps the text above and below. The horizontal padding and margins work perfectly normally. This half-working behaviour is what makes it confusing: the effect appears, it just does not reserve any space.

The practical consequence is that you should never try to build a button, a badge or a nav item out of a plain inline element. The moment you want padding all round, a fixed size, or reliable spacing, change it to inline-block or block, or put it inside a flex container. Setting display: block on links inside a navigation list is one of the most common one-line fixes in real stylesheets, because it turns the whole padded area into a proper tap target.

Example
/* Ignored — a span is inline */
span { width: 200px; height: 50px; }

/* Renders, but does not push the neighbouring lines apart */
.highlight {
  background: #fdf2fa;
  padding: 20px;         /* overlaps the lines above and below */
  margin: 20px 0;        /* the vertical part is ignored */
}

/* The fix: give it a box */
.badge {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  background: #d1039e;
  color: #ffffff;
  font-size: 12px;
}

/* Links in a nav: block turns padding into a real tap target */
.nav li a {
  display: block;
  padding: 12px 16px;
}
Notes
  • <img> is inline by default, which is why an image inside a container often has a few unexplained pixels of space beneath it — the browser is leaving room for the descenders of a text line. Setting display: block on the image, or vertical-align: middle, removes that gap.

inline-block and Its Whitespace Problem

inline-block is the compromise: the element sits in a line alongside its neighbours like inline content, but internally it behaves like a block, so width, height, vertical padding and vertical margins all work properly. Badges, buttons and small tags are its natural home.

It has one famous quirk. Because inline-block elements sit in a line of text, the whitespace in your HTML between them counts as a space character and is rendered. Put three inline-block cards on three separate lines in your HTML and you get roughly four pixels of gap between each pair that you never asked for and cannot remove with margin: 0. It is not a bug in your CSS — it is a literal space in your markup.

People used to fight this by writing the HTML with no line breaks between elements, or by setting font-size: 0 on the parent and restoring it on the children. Both are unpleasant. The real answer today is to stop using inline-block for layout: put the items in a flex or grid container instead, where whitespace between children is ignored entirely and gap gives you exact control. Keep inline-block for single elements that genuinely need to sit inside a sentence.

Example
/* Three cards, and three mystery gaps */
.card { display: inline-block; width: 30%; }

<div class="card">One</div>
<div class="card">Two</div>   <!-- the line breaks become spaces -->
<div class="card">Three</div>

/* The modern answer — no whitespace, exact spacing */
.cards {
  display: flex;
  gap: 16px;
}
.cards .card { flex: 1; }

/* inline-block is still right for something inside a sentence */
p .kbd {
  display: inline-block;
  padding: 2px 6px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-family: monospace;
}
Notes
  • If you meet an older tutorial that uses float to place boxes side by side, treat it as history. Floats were designed to wrap text around an image, which they still do well; using them for page layout means clearfix hacks and unequal column heights, and flexbox and grid replaced them years ago.

Hiding Things: none, hidden, and opacity: 0

There are three ways to make something invisible and they are genuinely different. Picking the wrong one causes bugs that are invisible on screen and obvious to anyone using a keyboard or a screen reader.

display: none removes the element from the layout completely. It occupies no space, everything after it moves up to fill the gap, and it is taken out of the accessibility tree so screen readers do not announce it and it cannot be reached by tabbing. This is what you want for content that is genuinely not present right now — a closed accordion panel, an inactive tab.

visibility: hidden hides the element but keeps its space. The layout does not move; there is simply a hole where the element was. It is also removed from the accessibility tree and cannot be focused. Use it when hiding something must not cause the rest of the page to jump — the classic case is a validation message whose space is reserved so the form does not shift when an error appears.

opacity: 0 is the dangerous one. The element is fully transparent but is still entirely there: it holds its space, it is still clickable, it can still receive keyboard focus, and screen readers still read it out. An "invisible" button hidden this way is an invisible trap — a keyboard user tabs onto something they cannot see. Use opacity for fading, and pair it with visibility when the element must actually become unavailable at the end of the fade.

Example
/* Gone: no space, no screen reader, no tab stop */
.tab-panel[hidden] { display: none; }

/* Invisible but the space stays; also unreachable */
.form-error { visibility: hidden; }
.form-error.is-shown { visibility: visible; }

/* Transparent but fully present — still focusable and announced */
.ghost { opacity: 0; }   /* rarely what you want on its own */

/* Fading out properly: opacity animates, visibility switches at the end */
.tooltip {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease, visibility 0.2s;
}
.has-tooltip:hover .tooltip {
  opacity: 1;
  visibility: visible;
}
block, inline, inline-block and three ways to hide
HTML
<h3>Display types</h3>
<div class="demo">
  <span class="chip block">block</span>
  <span class="chip block">block</span>
  <span class="chip inline">inline</span>
  <span class="chip inline">inline</span>
  <span class="chip ib">inline-block</span>
  <span class="chip ib">inline-block</span>
</div>

<h3>Three ways to hide the middle box</h3>
<div class="demo row">
  <div class="tile">1</div>
  <div class="tile gone">2</div>
  <div class="tile">3</div>
</div>
<p class="note">Change <code>.gone</code> between <code>display: none</code>, <code>visibility: hidden</code> and <code>opacity: 0</code> and watch what happens to the gap.</p>
CSS
body {
  font-family: system-ui, Arial, sans-serif;
  padding: 24px;
  background: #f4f4f7;
}

h3 { font-size: 15px; color: #555555; margin: 24px 0 8px; }

.demo {
  background: #ffffff;
  border: 1px dashed #b9b9c6;
  padding: 12px;
}

.chip {
  background: #d1039e;
  color: #ffffff;
  padding: 8px 12px;
  margin: 4px;
}

/* Fills its own line */
.block { display: block; }

/* Sits in the line; width, height and vertical margins are ignored */
.inline { display: inline; width: 200px; height: 60px; }

/* Sits in the line but accepts a real box */
.ib { display: inline-block; width: 140px; }

.row { display: flex; gap: 12px; }

.tile {
  width: 70px;
  height: 70px;
  background: #7a0060;
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}

/* Try each of these one at a time */
.gone { display: none; }
/* .gone { visibility: hidden; } */
/* .gone { opacity: 0; } */

.note { font-size: 14px; color: #555555; max-width: 60ch; }
  • display: none — no space, not announced, not focusable
  • visibility: hidden — keeps its space, not announced, not focusable
  • opacity: 0 — keeps its space, still announced, still clickable and focusable
  • The HTML hidden attribute is the markup equivalent of display: none
  • An element switched to display: none cannot fade — the change is instant, so animate opacity and switch visibility instead
  • display: contents removes the element's own box while keeping its children in the layout, which is occasionally useful for unwrapping a container inside a grid
Ask AI