Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 9 – Normal flow, positioning and stacking

Normal flow and the display property

How block and inline boxes flow, the two-value display syntax, inline-block and the gap under images, floats with shape-outside, and hiding content properly.

  • Beginner
  • 30 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Explain how block and inline boxes are laid out in normal flow and how display changes an element's role
  • Use the two-value display syntax, such as block flow or inline flex, to set the outer and inner display types
  • Predict when floats, positioned boxes and flex items are blockified
  • Remove the gap under inline images and give badges a size with inline-block
  • Wrap text around a floated pull quote along a circle with shape-outside
  • Choose between display, visibility, the hidden attribute and content-visibility to hide content correctly

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how block and inline boxes are laid out in normal flow and how display changes an element's role
  • Use the two-value display syntax, such as block flow or inline flex, to set the outer and inner display types
  • Predict when floats, positioned boxes and flex items are blockified
  • Remove the gap under inline images and give badges a size with inline-block
  • Wrap text around a floated pull quote along a circle with shape-outside
  • Choose between display, visibility, the hidden attribute and content-visibility to hide content correctly

Before you start

On this page

Normal flow is how a browser lays out boxes when nothing else is asked for. Block boxes, such as paragraphs and headings, stack from top to bottom, each as wide as its container. Inline boxes, such as links and strong text, sit side by side in lines of text and wrap onto the next line when one is full. The display property decides which of these roles an element plays, and it answers two questions at once: the outer type, block or inline, says how the box sits among its siblings, and the inner type, such as flow, flex or grid, says how it lays out its own children. So inline-flex means inline flex. This lesson measures each role in Chromium 153, Firefox 155 and WebKit 26.6, then covers inline-block and the gap under images, floats that wrap text along a circle, and the right way to hide content.

Block and inline boxes

In normal flow every box is either block-level or inline-level. A block-level box starts on a new line and, unless it has a width of its own, fills the inline size of its container. Blocks stack one after another in the block direction, top to bottom in English, and their vertical margins can collapse, as the margin collapse lesson measured. An inline-level box goes into a line box, one after another along the line; when the line is full, the rest moves to the next line, and an inline box can be split across two lines. On a non-replaced inline box such as a span, width and height do nothing, vertical margins don’t move anything, and vertical padding is painted over the lines around it without pushing them apart. Images and other replaced elements are inline too, but they keep the size you give them.

When text sits directly inside a block next to block children, the browser wraps it in an anonymous block box, so every block container ends up holding either only blocks or only lines.

The explorer gives the same two span elements, each with width: 120px, height: 40px, padding: 4px and margin: 10px, five different display values, in a 400 px row with words around them:

Five display values, measured HTML · display-explorer.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Five display values side by side</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .row { inline-size: 400px; margin-block-end: 16px; background: #eee; }
    /* The same two spans in every row: a width, a height, a margin and padding on all four sides. */
    .kid { inline-size: 120px; block-size: 40px; margin: 10px; padding: 4px; background: #dde; }
    #block .kid { display: block; }
    #inline .kid { display: inline; }
    #inline-block .kid { display: inline-block; }
    #flex .kid { display: flex; }
    #grid .kid { display: grid; }
  </style>
</head>
<body>
  <div class="row" id="block">Text <span class="kid">one</span> and <span class="kid">two</span> end.</div>
  <div class="row" id="inline">Text <span class="kid">one</span> and <span class="kid">two</span> end.</div>
  <div class="row" id="inline-block">Text <span class="kid">one</span> and <span class="kid">two</span> end.</div>
  <div class="row" id="flex">Text <span class="kid">one</span> and <span class="kid">two</span> end.</div>
  <div class="row" id="grid">Text <span class="kid">one</span> and <span class="kid">two</span> end.</div>
  <script>
    const px = (n) => Math.round(n * 100) / 100; // engines measure in fractions of a pixel
    for (const row of document.querySelectorAll('.row')) {
      const box = row.getBoundingClientRect();
      const [one, two] = [...row.querySelectorAll('.kid')].map((k) => k.getBoundingClientRect());
      const where = one.top === two.top ? 'side by side' : 'one above the other';
      console.log(`${row.id}: each span ${px(one.width)} x ${px(one.height)}, ${where}, ` +
        `first at x ${px(one.left - box.left)}, row ${px(box.height)}px tall`);
    }
  </script>
</body>
</html>
  • block: each span 128 × 48 px, on a line of its own, 10 px from the left; the row is 208 px tall.
  • inline: each span 34.7 × 25 px, in the line after the word “Text”; the row stays 24 px tall.
  • inline-block: each span 128 × 48 px, both in one line; the row is 92 px tall.
  • flex and grid: each span 128 × 48 px, on a line of its own; the row is 208 px tall.

The 128 px is the 120 px width plus 4 px of padding on each side. As inline, the span ignored its width and height: it was as wide as the word “one” plus its padding, and the row stayed a single 24 px line, because its vertical padding and margins changed nothing. The inline span’s height differed slightly, 25 px in Chromium, 27 px in Firefox and 25.88 px in WebKit, because each engine reads the font’s height in its own way; every other number agreed. From the outside, flex and grid behaved exactly like block. The difference is inside, where each span now lays out its word as a flex or grid item. That is the outer and the inner type at work.

Two keywords: the outer and the inner display type

display: inline flex split in two: the outer type inline places the box in a line, the inner type flex lays out its children.display: inline flex(one word: inline-flex)1. Outer type: inlineHow the box takes part in its parent's layout:it sits in a line of text, beside words2. Inner type: flexHow the box lays out its own children:they become flex itemsOne keyword sets one type, the other defaults:block = block flow · inline = inline flowflex = block flex · inline-block = inline flow-rootfirst keywordsecond keywordshorthands

The two keywords of a display value

Text description of the diagram

The diagram is a column of four boxes that takes the value display: inline flex apart. Its one-word form is inline-flex.

  1. The first keyword, inline, is the outer type. It decides how the box takes part in its parent's layout: the box sits in a line of text, beside words.
  2. The second keyword, flex, is the inner type. It decides how the box lays out its own children: they become flex items.
  3. The last box lists the shorthands. One keyword sets one type and the other takes its default: block means block flow, inline means inline flow, flex means block flex, and inline-block means inline flow-root.

CSS Display 3 splits every display value into an outer display type, how the box takes part in its parent’s flow layout, and an inner display type, the kind of formatting context it creates for its children. The two-value syntax writes both: display: block flow, display: inline flex, display: inline flow-root. It is Baseline widely available. The one-word values you already know are short forms of these pairs, and a single keyword takes the default for the half it leaves out: an outer keyword gets flow inside, and an inner keyword gets block outside.

The one-word values as pairs JavaScript · display_pairs.mjs
// The one-keyword display values and the two-value form each one stands for (CSS Display 3, section 2).
// The first word is the outer display type: how the box takes part in its parent's layout.
// The second is the inner display type: how the box lays out its own children.
// getComputedStyle() reports the short form, so `display: inline flex` reads back as "inline-flex".

const pairs = {
  block: ['block', 'flow'],
  inline: ['inline', 'flow'],
  'flow-root': ['block', 'flow-root'],
  'inline-block': ['inline', 'flow-root'],
  flex: ['block', 'flex'],
  'inline-flex': ['inline', 'flex'],
  grid: ['block', 'grid'],
  'inline-grid': ['inline', 'grid'],
  table: ['block', 'table'],
  'inline-table': ['inline', 'table'],
};

const role = { block: 'starts a new line', inline: 'sits in a line of text' };
const children = {
  flow: 'block and inline flow',
  'flow-root': 'flow, in its own formatting context',
  flex: 'flex items',
  grid: 'grid items',
  table: 'rows and cells',
};

for (const [short, [outer, inner]] of Object.entries(pairs)) {
  console.log(`${short.padEnd(13)} = ${`${outer} ${inner}`.padEnd(17)} ${role[outer]}; children: ${children[inner]}`);
}

Output

block         = block flow        starts a new line; children: block and inline flow
inline        = inline flow       sits in a line of text; children: block and inline flow
flow-root     = block flow-root   starts a new line; children: flow, in its own formatting context
inline-block  = inline flow-root  sits in a line of text; children: flow, in its own formatting context
flex          = block flex        starts a new line; children: flex items
inline-flex   = inline flex       sits in a line of text; children: flex items
grid          = block grid        starts a new line; children: grid items
inline-grid   = inline grid       sits in a line of text; children: grid items
table         = block table       starts a new line; children: rows and cells
inline-table  = inline table      sits in a line of text; children: rows and cells

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node display_pairs.mjs

Browsers report a display value in its shortest backwards-compatible form, and the two keywords may come in either order, so display: flex inline reads back from getComputedStyle() as inline-flex:

What getComputedStyle() reports for display HTML · two-values.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>What getComputedStyle reports for display</title>
  <style>
    /* Two-value forms, in either order. data-written repeats each value for the log. */
    .a { display: inline flex; }
    .b { display: flex inline; }
    .c { display: block flow; }
    .d { display: flow-root block; }
    .e { display: inline flow-root; }
    .f { display: block grid; }
    .g { display: inline list-item; }
    /* Boxes that get blockified: floated, absolutely positioned, or a flex item. */
    .f1 { float: left; display: inline; }
    .f2 { float: left; display: inline-block; }
    .f3 { float: left; display: inline-flex; }
    .f4 { position: absolute; display: inline-grid; }
    .f5 { float: left; display: inline-table; }
    .items { display: flex; }
    .i1 { display: inline; }
    .i2 { display: inline-block; }
    .i3 { display: inline-flex; }
    .i4 { display: table-cell; }
    .i5 { display: contents; }
  </style>
</head>
<body>
  <div>
    <span class="a" data-written="inline flex"></span>
    <span class="b" data-written="flex inline"></span>
    <span class="c" data-written="block flow"></span>
    <span class="d" data-written="flow-root block"></span>
    <span class="e" data-written="inline flow-root"></span>
    <span class="f" data-written="block grid"></span>
    <span class="g" data-written="inline list-item"></span>
  </div>
  <div>
    <span class="f1" data-written="inline, floated"></span>
    <span class="f2" data-written="inline-block, floated"></span>
    <span class="f3" data-written="inline-flex, floated"></span>
    <span class="f4" data-written="inline-grid, absolute"></span>
    <span class="f5" data-written="inline-table, floated"></span>
  </div>
  <div class="items">
    <span class="i1" data-written="inline, flex item"></span>
    <span class="i2" data-written="inline-block, flex item"></span>
    <span class="i3" data-written="inline-flex, flex item"></span>
    <span class="i4" data-written="table-cell, flex item"></span>
    <span class="i5" data-written="contents, flex item"></span>
  </div>
  <script>
    for (const el of document.querySelectorAll('[data-written]')) {
      console.log(`${el.dataset.written.padEnd(24)} -> ${getComputedStyle(el).display}`);
    }
  </script>
</body>
</html>

All three engines agreed on every pair but one. WebKit 26.6 rejected inline list-item, so its span fell back to inline, while Chromium and Firefox kept it as inline list-item: a feature’s Baseline status doesn’t promise that every combination of it works. Most code only needs the one-word forms. The two-value form earns its place where it says more, such as inline flow-root, a box that sits in a line but keeps its floats and margins inside, and it is the clearest way to read what the one-word forms mean.

Floats, positioned boxes and flex items become blocks

Some layouts change the outer type for you. A float, an absolutely or fixed positioned box, a flex or grid item and the root element are blockified: their outer type becomes block whatever you wrote. The same page measured it:

Written Result
inline, floated block
inline-block, floated block
inline-flex, floated flex
inline-grid, absolute grid
inline-table, floated table
inline, a flex item block
inline-block, a flex item block
table-cell, a flex item block
contents, a flex item contents

A blockified inline-block becomes a plain block, not flow-root. The specification keeps that rule for compatibility with CSS 2, and nothing is lost, because floats, positioned boxes and flex and grid items all start their own formatting context anyway. Blockification is also why display: inline on a flex item changes nothing, and why a floated span takes the width you give it.

inline-block, and the gap under images

inline-block, in full inline flow-root, is the box for things that belong in a line of text but need a size: a badge, a link styled as a button, an icon with padding. From the outside it is a word in the line. Inside, it is a block container with its own formatting context, so width, height, padding and margins all apply, and its floats and its children’s margins stay inside it. A “New” badge with width: 80px, height: 30px, padding: 4px 8px and margin: 12px, in a 320 px paragraph:

A badge as inline and inline-block, and the gap under an image HTML · inline-boxes.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A badge as inline and inline-block, and the gap under an image</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    p { inline-size: 320px; margin: 0 0 16px; }
    .badge { inline-size: 80px; block-size: 30px; padding: 4px 8px; margin: 12px; background: #fd8; }
    #as-inline-block .badge { display: inline-block; }

    /* An image 80px tall in a box with no padding: any space below it is a gap. */
    .frame { inline-size: 300px; margin-block-end: 8px; background: #eee; }
    .frame img { inline-size: 120px; block-size: 80px; background: #99c; }
    #middle img { vertical-align: middle; }
    #bottom img { vertical-align: bottom; }
    #block img { display: block; }
    #line-height-normal { line-height: normal; }
    #icon-middle img { inline-size: 16px; block-size: 16px; vertical-align: middle; }
  </style>
</head>
<body>
  <p id="as-inline">A badge <span class="badge">New</span> sits in this line of text, which wraps onto a second line.</p>
  <p id="as-inline-block">A badge <span class="badge">New</span> sits in this line of text, which wraps onto a second line.</p>

  <div class="frame" id="baseline"><img alt=""></div>
  <div class="frame" id="line-height-normal"><img alt=""></div>
  <div class="frame" id="middle"><img alt=""></div>
  <div class="frame" id="bottom"><img alt=""></div>
  <div class="frame" id="block"><img alt=""></div>
  <div class="frame" id="icon-middle"><img alt=""></div>
  <script>
    const px = (n) => Math.round(n * 100) / 100;
    for (const p of document.querySelectorAll('p')) {
      const badge = p.querySelector('.badge').getBoundingClientRect();
      console.log(`${p.id}: badge ${px(badge.width)} x ${px(badge.height)}, paragraph ${px(p.getBoundingClientRect().height)}px tall`);
    }
    for (const frame of document.querySelectorAll('.frame')) {
      const gap = frame.getBoundingClientRect().bottom - frame.querySelector('img').getBoundingClientRect().bottom;
      console.log(`${frame.id}: ${px(gap)}px below the image`);
    }
  </script>
</body>
</html>
  • As inline, the badge measured 48 × 25 px, and the paragraph kept its two lines of 24 px, 48 px in all.
  • As inline-block, it measured 96 × 38 px, and the paragraph grew to 86 px, because the first line made room for it.

An inline-block sits on the baseline of its last line of text by default, and the line grows to hold it and its margins; vertical-align lines it up another way.

Images are inline by default, and that causes one of the most asked-about gaps in CSS: a few pixels of space under an image in a box with no padding. An inline image sits on the baseline, where the bottom of the letter x is, and every line box keeps room below the baseline for descenders, the tails of g, p and y. The program works out that room from Arial’s metrics:

Where the gap under an inline image comes from JavaScript · image_gap.mjs
// Why an inline image has a gap under it: it sits on the baseline, and the line box keeps room below the
// baseline for the descenders of letters such as g and p (CSS 2, section 10.8: the "strut").
// Arial's metrics, in font units of a 2,048-unit em: 1,854 above the baseline and 434 below it.

const ascent = 1854 / 2048;
const descent = 434 / 2048;

function gapBelowImage(fontSize, lineHeight) {
  const content = (ascent + descent) * fontSize;    // the height of the font's glyph area
  const halfLeading = (lineHeight - content) / 2;   // line-height adds half of the rest above and half below
  return descent * fontSize + halfLeading;          // what stays under the baseline
}

for (const [fontSize, lineHeight] of [[16, 24], [16, 32], [20, 30]]) {
  const gap = gapBelowImage(fontSize, lineHeight);
  console.log(`font-size ${fontSize}px, line-height ${lineHeight}px: ${gap.toFixed(2)}px under the image`);
}
console.log('vertical-align: bottom or display: block: 0px under the image (it no longer sits on the baseline)');

Output

font-size 16px, line-height 24px: 6.45px under the image
font-size 16px, line-height 32px: 10.45px under the image
font-size 20px, line-height 30px: 8.07px under the image
vertical-align: bottom or display: block: 0px under the image (it no longer sits on the baseline)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node image_gap.mjs

The same page measured an 80 px image in a 24 px line, and a 16 px icon:

The image has Gap below it
the defaults 6.45 px in WebKit, 6.5 px in Firefox, 7 px in Chromium
a parent with line-height: normal 3.45 to 4.27 px
vertical-align: middle 0 px
vertical-align: bottom 0 px
display: block 0 px
vertical-align: middle, as a 16 px icon 2.61 to 3.16 px

WebKit’s 6.45 px is exactly the program’s answer; Firefox and Chromium round the font metrics before they add them up. For an image that stands on its own, such as the photo of a card, display: block is the clean fix. For an icon that belongs in a line of text, keep it inline and use vertical-align: bottom, or middle to centre it on the text. middle removed the gap for the 80 px image only because the image is taller than the line: the 16 px icon still had about 3 px under it.

Floats, and text that follows a shape

A float is taken out of normal flow and pushed to the left or right of its container, and the lines of text that follow it get shorter, so they wrap around it. Pages are no longer laid out with floats; flexbox and grid do that job. Floats remain the way to wrap text around something, such as a figure, a pull quote or a drop cap. Three rules cover most of their behaviour. Block boxes next to a float ignore it, so their backgrounds run under it, while their lines wrap around it. clear moves a box down below earlier floats. And a container doesn’t grow to hold its floats unless it starts a block formatting context, which is what display: flow-root is for. float: inline-start and float: inline-end are the logical forms of left and right, and they follow the direction of the text.

By default the lines wrap around the float’s margin box, a rectangle, even when the float looks round. shape-outside gives the float a shape for the lines to follow instead, such as circle(), ellipse(), polygon() or the transparent parts of an image, and shape-margin adds space around that shape. A round pull quote, 160 px across, in a 340 px column, with the start of each line measured:

A floated pull quote, with and without shape-outside HTML · pull-quote.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A floated pull quote, with and without shape-outside</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    article { inline-size: 340px; margin-block-end: 24px; }
    .quote {
      float: inline-start;
      inline-size: 160px;
      block-size: 160px;
      margin-inline-end: 12px;
      border-radius: 50%;            /* the circle you see */
      background: #cde;
    }
    #circle .quote {
      shape-outside: circle(50%);    /* the circle the lines follow, measured against the margin box */
      shape-margin: 12px;
    }
    #circle-fixed .quote {
      shape-outside: circle(50%) border-box;  /* measured against the circle you see */
      shape-margin: 12px;
      margin-inline-end: 24px;       /* a shape never reaches past the margin box, so leave room for it */
    }
  </style>
</head>
<body>
  <article id="box"><div class="quote"></div><p>Floats take a box out of the line and push it to one side, and the lines of text that follow wrap around it. With shape-outside the lines follow a circle instead of the box, so each line starts a little further left or right than the one before it, as the curve of the circle passes.</p></article>
  <article id="circle"><div class="quote"></div><p>Floats take a box out of the line and push it to one side, and the lines of text that follow wrap around it. With shape-outside the lines follow a circle instead of the box, so each line starts a little further left or right than the one before it, as the curve of the circle passes.</p></article>
  <article id="circle-fixed"><div class="quote"></div><p>Floats take a box out of the line and push it to one side, and the lines of text that follow wrap around it. With shape-outside the lines follow a circle instead of the box, so each line starts a little further left or right than the one before it, as the curve of the circle passes.</p></article>
  <script>
    // Where each line of the paragraph starts, measured one character at a time.
    for (const article of document.querySelectorAll('article')) {
      const p = article.querySelector('p');
      const text = p.firstChild;
      const left = p.getBoundingClientRect().left;
      const range = document.createRange();
      const starts = new Map();
      for (let i = 0; i < text.length; i++) {
        range.setStart(text, i);
        range.setEnd(text, i + 1);
        const r = range.getClientRects()[0];
        if (r && r.width > 0 && !starts.has(Math.round(r.top))) starts.set(Math.round(r.top), Math.round(r.left - left));
      }
      console.log(`${article.id}: lines start at ${[...starts.values()].slice(0, 8).join(', ')} px`);
    }
  </script>
</body>
</html>
  • No shape, with a 12 px margin: all seven lines start at 172 px.
  • circle(50%) with shape-margin: 12px: the lines start at 163, 172, 172, 172, 172, 172 and 156 px.
  • circle(50%) border-box with shape-margin: 12px and a 24 px margin: 153, 166, 172, 172, 171, 163 and 146 px.

Two rules explain the middle case. A shape is measured against the float’s margin box unless you name another box, so circle(50%) was centred 6 px to the right of the circle you see. And a shape never reaches past the margin box, so a 12 px margin cut off the 12 px shape margin on five of the lines. Measuring against border-box and giving the float a margin at least as wide as the shape margin lets the lines follow the circle all the way round, as the last case shows; WebKit started some of those lines 1 px further left. shape-outside is Baseline widely available, and it changes only how the text wraps: border-radius or clip-path changes how the float looks.

Hiding content: what each technique removes

Hiding something is easy; hiding it from the right people is the skill. display: none removes the element and everything inside it from the layout, from the keyboard’s tab order and from the accessibility tree that screen readers use. visibility: hidden makes the box invisible but keeps its space; it also takes the element out of the tab order and the accessibility tree, and a descendant can set visibility: visible to show again. opacity: 0 only makes the box transparent: it keeps its space, and everything in it stays focusable and is still read out. content-visibility: hidden keeps the element’s own box but skips its contents, which can’t be focused, selected or found with find-in-page; it is Baseline newly available. The HTML hidden attribute is display: none from the browser’s own style sheet, and hidden="until-found" is content-visibility: hidden that find-in-page and links to a fragment inside it can open.

The lab hides the same panel, a flex box with 8 px of padding and a link inside, eleven ways, between two markers:

Hiding one panel eleven ways HTML · hiding.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Six ways to hide a panel, measured</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .marker { block-size: 10px; }
    /* A component rule like many sites have: it sets display, so it beats the browser's [hidden] rule. */
    .panel { display: flex; gap: 8px; padding: 8px; border: 1px solid; }
    .none { display: none; }
    .invisible { visibility: hidden; }
    .invisible .shown-again { visibility: visible; }
    .skipped { content-visibility: hidden; }
    .transparent { opacity: 0; }
    /* Two guard rules: the first also catches hidden="until-found"; the second leaves it alone. */
    .guard-all [hidden] { display: none !important; }
    .guard-safe [hidden]:not([hidden="until-found" i]) { display: none !important; }
  </style>
</head>
<body>
  <div class="case" id="display-none"><div class="marker"></div><div class="panel none"><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case" id="visibility-hidden"><div class="marker"></div><div class="panel invisible"><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case" id="visibility-child-visible"><div class="marker"></div><div class="panel invisible"><a href="#a">A link</a><a class="shown-again" href="#b">Back</a></div><div class="marker"></div></div>
  <div class="case" id="content-visibility-hidden"><div class="marker"></div><div class="panel skipped"><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case" id="opacity-0"><div class="marker"></div><div class="panel transparent"><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case" id="hidden-plain-div"><div class="marker"></div><div hidden><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case" id="hidden-on-panel"><div class="marker"></div><div class="panel" hidden><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case guard-all" id="hidden-guard-all"><div class="marker"></div><div class="panel" hidden><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case" id="until-found"><div class="marker"></div><div class="panel" hidden="until-found"><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case guard-all" id="until-found-guard-all"><div class="marker"></div><div class="panel" hidden="until-found"><a href="#a">A link</a></div><div class="marker"></div></div>
  <div class="case guard-safe" id="until-found-guard-safe"><div class="marker"></div><div class="panel" hidden="until-found"><a href="#a">A link</a></div><div class="marker"></div></div>
  <script>
    const px = (n) => Math.round(n * 100) / 100;
    for (const c of document.querySelectorAll('.case')) {
      const [above, panel, below] = c.children;
      const link = panel.querySelector('a');
      const space = px(below.getBoundingClientRect().top - above.getBoundingClientRect().bottom);
      link.focus();
      const focusable = document.activeElement === link;
      link.blur();
      let line = `${c.id}: panel boxes ${panel.getClientRects().length}, space ${space}px, link boxes ${link.getClientRects().length}, ` +
        `link focusable ${focusable}`;
      const back = panel.querySelector('.shown-again');
      if (back) {
        back.focus();
        line += `, "Back" focusable ${document.activeElement === back}`;
        back.blur();
      }
      console.log(line);
    }
  </script>
</body>
</html>
  • display: none: no box, 0 px of space; the link can’t take focus.
  • visibility: hidden: one box, 42 px of space; the link can’t take focus. A child set to visible shows again and takes focus.
  • content-visibility: hidden: one box, 18 px of space; the link can’t take focus.
  • opacity: 0: one box, 42 px of space, and the link still takes focus.
  • hidden on a plain div: no box, 0 px of space; no focus.
  • hidden on .panel { display: flex }: one box, 42 px of space, and the panel shows: its link takes focus.
  • The same with [hidden] { display: none !important }: no box, 0 px of space; no focus.
  • hidden="until-found": one box, 18 px of space; no focus.
  • until-found with the same guard: no box, 0 px of space; no focus.
  • until-found with the guard below: one box, 18 px of space; no focus.

The 42 px is the panel’s 24 px line plus 16 px of padding and 2 px of border. The 18 px that content-visibility: hidden leaves is the padding and border alone, because skipping the contents also sizes the box as if it were empty. All three engines gave these numbers, with one difference: Chromium reported no boxes for the link inside a skipped panel, while Firefox and WebKit reported one. So don’t count boxes to find out whether something is visible; the element.checkVisibility() method, Baseline widely available, exists for that question.

Two of those results are bugs: “the link still takes focus” and “the panel shows”. opacity: 0 hides nothing from a keyboard user, who can tab into a link they can’t see. And the hidden attribute loses to any site rule that sets display, because the browser’s [hidden] rule is an ordinary declaration in the browser’s own style sheet, and every normal declaration of the site beats it whatever the selectors. The HTML Standard warns about exactly this. The fix is a guard rule, written so that it leaves until-found alone: a plain [hidden] selector also matches hidden="until-found", and in the lab it turned those panels into display: none, which find-in-page can’t open.

A guard rule for the hidden attribute CSS · hidden-guard.css
/* The hidden attribute should beat component rules such as .panel { display: flex }.
   !important beats every normal rule, and in the first layer it also beats the !important rules of later layers
   and of unlayered CSS. The :not() part leaves hidden="until-found" alone, because that state hides with
   content-visibility and must keep its box. */
@layer base, components, utilities;

@layer base {
  [hidden]:not([hidden="until-found" i]) {
    display: none !important;
  }
}

hidden="until-found" itself has limited availability: Chromium and Firefox support it, Safari doesn’t yet. WebKit 26.6 hid the panel like the others, so give the content a second way in, such as the button of an accordion.

display: contents removes only the element’s own box

display: contents removes the element’s own box and keeps its children’s, as if the children stood in its place. A wrapper div with display: contents lets its children become flex or grid items of the wrapper’s parent:

A wrapper and a button with display: contents HTML · contents.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>What display: contents removes</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .row { display: flex; gap: 8px; }
    .row span { padding: 4px 8px; background: #dde; }
    .wrapper { display: contents; border: 2px solid; } /* the border has no box to be drawn on */
    .unboxed { display: contents; }
  </style>
</head>
<body>
  <div class="row"><div class="wrapper" id="wrapper"><span>One</span><span>Two</span></div><span>Three</span></div>
  <p><button type="button" class="unboxed" id="save">Save</button> <button type="button" id="cancel">Cancel</button></p>
  <script>
    const wrapper = document.getElementById('wrapper');
    const tops = [...document.querySelectorAll('.row span')].map((s) => Math.round(s.getBoundingClientRect().top));
    console.log(`wrapper: ${wrapper.getClientRects().length} boxes; its two spans and the third sit in one flex row: ${new Set(tops).size === 1}`);
    for (const id of ['save', 'cancel']) {
      const button = document.getElementById(id);
      button.focus();
      console.log(`${id} button: ${button.getClientRects().length} boxes, focus() focused it: ${document.activeElement === button}`);
      button.blur();
    }
  </script>
</body>
</html>

The wrapper had no box, and its two spans lined up in the flex row with the third. The button with display: contents had no box either, and focus() could not focus it in any of the three engines, although the HTML Standard says such an element stays focusable; the button next to it focused normally. Browsers have also shipped bugs that dropped elements with display: contents from the accessibility tree, so a button, heading or list could stop being one for a screen reader, and web-features lists display: contents as limited availability. Use it only on wrappers that carry no meaning, and never on interactive elements.

HTML, CSS & JS Playground (Online Editor) Paste any of the lab pages into the HTML pane and inspect the boxes with DevTools.

Practise: computed display values and boxes

The exercise turns the measurements of this lesson into two functions, and its tests are the cases of the labs.

Exercise · Medium · JavaScript

Predict computed display values and which panels keep a box

Two questions come up whenever a layout surprises you: which display value did the browser really use, and does the element have a box at all? Write two functions in display.mjs and export them.

The first, computedDisplay(value, { blockified = false } = {}), takes a display value as written in CSS and returns what getComputedStyle() reports for it, or null when the browser would reject the value:

  • One keyword stays as it is: block, inline, flow-root, inline-block, flex, inline-flex, grid, inline-grid, table, inline-table, list-item, none, contents, and the table parts such as table-cell.
  • flow alone means block flow, so it reads back as block.
  • Two keywords, in either order, are one outer type (block or inline) and one inner type (flow, flow-root, flex, grid or table). Return the short form: block flow is block, block flow-root is flow-root, inline flow is inline, inline flow-root is inline-block, block flex is flex, inline flex is inline-flex, and the same pattern for grid and table. Any other pair, such as two outer types or inline contents, is invalid.
  • With blockified: true (the box floats, is absolutely positioned or is a flex or grid item), the outer type becomes block. An inline flow-root box becomes a plain block, a table part such as table-cell becomes block, and none, contents and list-item stay as they are.

The second, boxCount({ display = null, hidden = false, guard = 'none' } = {}), says how many boxes getClientRects() returns for a div panel: 0 or 1.

  • display is the value the site's CSS sets on the panel, or null when it sets none (a div is then a block).
  • hidden is the panel's hidden attribute: false, true or 'until-found'. The browser's own rule gives hidden panels display: none, but any display the site sets beats it. 'until-found' hides with content-visibility: hidden, which keeps the box.
  • guard is the site's guard rule: 'none', 'all' for [hidden] { display: none !important; }, or 'not-until-found' for the same rule on [hidden]:not([hidden="until-found" i]). A guard wins over every other display rule for the panels it matches.
  • display: none and display: contents leave no box of the panel's own.

The sample tests import both functions from display.mjs and run in your browser.

Starter code · display.mjs

/** What getComputedStyle() reports for a display value, or null when the browser rejects it. */
export function computedDisplay(value, { blockified = false } = {}) {
  // Replace this line with your code.
  return value;
}

/** How many boxes getClientRects() returns for a div panel: 0 or 1. */
export function boxCount({ display = null, hidden = false, guard = 'none' } = {}) {
  // Replace this line with your code.
  return 1;
}
The sample tests · display.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { computedDisplay, boxCount } from './display.mjs';

test('one keyword reads back as written', () => {
  for (const v of ['block', 'inline', 'flow-root', 'inline-block', 'flex', 'inline-flex', 'grid', 'table', 'list-item', 'none', 'contents']) {
    assert.equal(computedDisplay(v), v);
  }
});

test('two keywords read back in the short form, in either order', () => {
  assert.equal(computedDisplay('inline flex'), 'inline-flex');
  assert.equal(computedDisplay('flex inline'), 'inline-flex');
  assert.equal(computedDisplay('block flow'), 'block');
  assert.equal(computedDisplay('flow-root block'), 'flow-root');
  assert.equal(computedDisplay('inline flow-root'), 'inline-block');
  assert.equal(computedDisplay('block grid'), 'grid');
  assert.equal(computedDisplay('inline table'), 'inline-table');
});

test('a keyword left out takes its default', () => {
  assert.equal(computedDisplay('flow'), 'block');
  assert.equal(computedDisplay('inline flow'), 'inline');
});

test('values the browser rejects', () => {
  assert.equal(computedDisplay('inline block'), null);
  assert.equal(computedDisplay('flex grid'), null);
  assert.equal(computedDisplay('flex-inline'), null);
  assert.equal(computedDisplay('inline contents'), null);
});

test('floats, absolutely positioned boxes and flex items are blockified', () => {
  const b = { blockified: true };
  assert.equal(computedDisplay('inline', b), 'block');
  assert.equal(computedDisplay('inline-block', b), 'block');
  assert.equal(computedDisplay('inline-flex', b), 'flex');
  assert.equal(computedDisplay('inline grid', b), 'grid');
  assert.equal(computedDisplay('inline-table', b), 'table');
  assert.equal(computedDisplay('table-cell', b), 'block');
  assert.equal(computedDisplay('contents', b), 'contents');
  assert.equal(computedDisplay('flex', b), 'flex');
});

test('the hidden attribute against the site\'s display rules', () => {
  assert.equal(boxCount({}), 1);
  assert.equal(boxCount({ hidden: true }), 0);
  assert.equal(boxCount({ display: 'flex', hidden: true }), 1);
  assert.equal(boxCount({ display: 'flex', hidden: true, guard: 'all' }), 0);
  assert.equal(boxCount({ display: 'none' }), 0);
  assert.equal(boxCount({ display: 'contents' }), 0);
});

test('hidden="until-found" keeps its box unless a guard sets display: none', () => {
  assert.equal(boxCount({ display: 'flex', hidden: 'until-found' }), 1);
  assert.equal(boxCount({ display: 'flex', hidden: 'until-found', guard: 'all' }), 0);
  assert.equal(boxCount({ display: 'flex', hidden: 'until-found', guard: 'not-until-found' }), 1);
});
A hint

Split the value on spaces. For one word, look it up: the legacy words map to a pair (inline-flex is inline flex), an outer word gets flow and an inner word gets block. For two words, find exactly one outer and one inner word. Then, if blockified, swap the outer word for block (watch inline flow-root), and turn the pair back into its short form with a lookup table keyed by "outer inner".

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A "New" badge must sit inside a line of text, and its width, height and vertical padding must take effect and push the lines apart. Which display value does that?

    Choose one answer.

    Show the answer to question 1

    Answer: inline-block

    inline-block, the same as inline flow-root, keeps the box in the line like a word while it lays out its inside as a block, so width, height, padding and margins all apply. In the lesson's run the badge measured 96 by 38 px and the paragraph grew from 48 to 86 px. As inline, the width was ignored and the paragraph kept its 48 px.

  2. Question 2 of 6 What does getComputedStyle(card).display report for this card?

    Read the code, then choose one answer.

    .card { display: flex inline; }
    Show the answer to question 2

    Answer: inline-flex

    The two keywords may come in either order: one outer type (inline) and one inner type (flex). Browsers report a display value in its shortest backwards-compatible form, so all three engines read it back as inline-flex.

  3. Question 3 of 6 A span has display: inline-block and float: left. What does getComputedStyle() report for its display?

    Choose one answer.

    Show the answer to question 3

    Answer: block

    A float is blockified: its outer type becomes block. For legacy reasons an inline flow-root box that is blockified becomes a plain block rather than flow-root, which is what Chromium, Firefox and WebKit all reported.

  4. Question 4 of 6 A settings panel has the hidden attribute, and the site's CSS has .panel { display: flex; }. What happens?

    Choose one answer.

    Show the answer to question 4

    Answer: The panel shows, because a site rule that sets display beats the browser's rule for [hidden].

    Browsers implement hidden with an ordinary rule in their own style sheet, [hidden] { display: none }. Any display declaration from the site beats a normal browser declaration, whatever the selectors, so the panel appeared and its link could take focus in all three engines. A guard rule with !important restores it.

  5. Question 5 of 6 Which of these leave the element taking up space in the layout?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • opacity: 0
    • visibility: hidden

    visibility: hidden and opacity: 0 keep the box and its space: 42 px for the lesson's panel. display: none and the hidden attribute (when nothing overrides it) remove the box, so the space shrinks to 0 px. Of the two that keep space, only opacity: 0 leaves the content focusable, so it hides nothing from keyboard users.

  6. Question 6 of 6 An inline image sits in a block with Arial at font-size: 16px and line-height: 32px. Using the lesson's program, how many pixels does the line box leave under the image?

    Type a number.

    Show the answer to question 6

    Answer: 10.45 px (anything from 10.35 to 10.55 counts)

    The image sits on the baseline. Below the baseline the line keeps Arial's descent, 434 of 2,048 units, which is 3.39 px at 16 px, plus half of the leading: (32 − 17.88) ÷ 2 = 7.06 px. Together that is 10.45 px. Setting display: block on the image, or vertical-align: bottom, removes the gap.

Interview questions

Fresher · What is the difference between display: none, visibility: hidden and opacity: 0?

display: none removes the element and its descendants from the layout, so the space closes up, and from the tab order and the accessibility tree. visibility: hidden keeps the box and its space but draws nothing; the element can’t take focus and screen readers skip it, while a descendant with visibility: visible still shows. opacity: 0 makes the element fully transparent and nothing else: it keeps its space, stays focusable and is still read out, so it is not a way to hide content from anyone. In a measured case, a panel hidden with visibility: hidden or opacity: 0 kept all 42 px of its space and display: none left 0 px, but only with opacity: 0 could its link still take focus. Use display: none or the hidden attribute to hide content from everyone.

Key takeaways

  • Block-level boxes stack and fill the line; inline-level boxes flow in lines, and on a span width, height and vertical margins do nothing.
  • display sets an outer type, how the box sits among its siblings, and an inner type, how it lays out its children: inline-flex is inline flex, inline-block is inline flow-root.
  • Floats, absolutely positioned boxes and flex and grid items are blockified, and a blockified inline-block becomes block.
  • inline-block gives an element in a line a size; an inline image leaves a gap for descenders, which display: block or vertical-align: bottom removes.
  • shape-outside makes text wrap along a shape; measure it against border-box and leave a margin as wide as the shape margin.
  • display: none and hidden remove the box; visibility: hidden keeps the space; opacity: 0 hides nothing from the keyboard. Guard hidden with [hidden]:not([hidden="until-found" i]) { display: none !important; } in the first layer, and keep display: contents off interactive elements.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.