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 4 – The box model, sizing and overflow

Overflow, scroll containers and truncation

What overflow visible, hidden, clip, scroll and auto do, which make scroll containers, why sticky stops sticking, and safe truncation and scrollbars.

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

What you will learn

  • Choose between overflow values visible, hidden, clip, scroll and auto, and tell which create scroll containers
  • Limit text to one line or several lines with an ellipsis, without hiding what matters
  • Prevent layout jumps from appearing scrollbars with scrollbar-gutter, and style scrollbars within limits
  • Fix long words and URLs that push a flex item out of a narrow container

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between overflow values visible, hidden, clip, scroll and auto, and tell which create scroll containers
  • Limit text to one line or several lines with an ellipsis, without hiding what matters
  • Prevent layout jumps from appearing scrollbars with scrollbar-gutter, and style scrollbars within limits
  • Fix long words and URLs that push a flex item out of a narrow container

Before you start

On this page

Content overflows when it is bigger than its box: a long word in a narrow column, a list taller than its panel, a table wider than a phone. The overflow property decides what happens. visible, the default, lets it spill out. hidden clips it at the padding edge, while scripts can still scroll it. clip clips it and never scrolls at all. scroll clips it and always shows scrollbars, and auto shows them only when something overflows. The three that can scroll, hidden, scroll and auto, make the box a scroll container, and that choice reaches further than it looks: it decides where a sticky header sticks, whether margins collapse through the box, and what focus and scripts can move. This lesson measures each effect in Chromium 153, Firefox 155 and WebKit 26.6, then covers truncating text, scrollbars that don’t shift the page, and long URLs.

The five values, sorted

The overflow values as a ladder: visible spills, clip clips, hidden is a scroll container, auto and scroll can be scrolled.visiblespills out · no clipping · nothing scrollsclipclips · not a scroll container · never scrolls(a sticky child looks past it)hiddenclips · a scroll container · no scrollbar(scripts and focus can scroll it)auto and scrollclips · a scroll container · people can scroll it(auto: scrollbars only when content overflows)clipsbecomes a scroll containerpeople can scroll

The five values of overflow, from spilling out to scrolling

Text description of the diagram

The diagram is a ladder of four steps. Each step down adds one ability.

  1. visible: content spills out of the box, nothing is clipped and nothing scrolls.
  2. One step down, clip: the content is clipped, but the box is not a scroll container and never scrolls, not even by a script. A sticky child looks past it to the next scroll container.
  3. The next step, hidden: the box clips and becomes a scroll container. It shows no scrollbar, but scripts and focus can scroll it, and a sticky child sticks to it.
  4. The last step, auto and scroll: the box clips, is a scroll container and people can scroll it. auto shows scrollbars only when the content overflows; scroll always shows them.

overflow is a shorthand for overflow-x and overflow-y, and the two can’t be set independently in every combination. A box can’t let content spill out along one axis while it scrolls along the other, so when one axis has a scrollable value, visible on the other computes to auto and clip computes to hidden. The program applies these rules to eight pairs:

From the values you write to what the browser uses JavaScript · overflow_rules.mjs
// The rules of CSS Overflow 3 for one box: from the overflow-x and overflow-y values you write to the values the
// browser computes, and what follows from them. Add your own pairs to the list and run it again.

const SCROLLABLE = new Set(['hidden', 'scroll', 'auto']); // the values that make a scroll container

function overflowFacts(x, y = x) {
  // visible and clip cannot sit next to a scrollable value: they compute to auto and hidden.
  const settle = (value, other) => {
    if (!SCROLLABLE.has(other)) return value;
    if (value === 'visible') return 'auto';
    if (value === 'clip') return 'hidden';
    return value;
  };
  const computed = [settle(x, y), settle(y, x)];
  const scrollContainer = computed.some((v) => SCROLLABLE.has(v));
  return {
    computed: computed.join(' '),
    clips: computed.some((v) => v !== 'visible'),
    scrollContainer,
    // A person can scroll it with a wheel, a finger or the keyboard only with scroll or auto.
    userScrolls: computed.some((v) => v === 'scroll' || v === 'auto'),
  };
}

const yesNo = (b) => (b ? 'yes' : 'no');
const row = (cells) => cells.map((c, i) => c.padEnd([18, 17, 7, 18, 0][i])).join('').trimEnd();
console.log(row(['you write', 'computes to', 'clips', 'scroll container', 'user can scroll']));
for (const [x, y] of [['visible'], ['hidden'], ['clip'], ['scroll'], ['auto'], ['visible', 'hidden'], ['clip', 'auto'], ['clip', 'visible']]) {
  const f = overflowFacts(x, y);
  console.log(row([y ? `${x} ${y}` : x, f.computed, yesNo(f.clips), yesNo(f.scrollContainer), yesNo(f.userScrolls)]));
}

Output

you write         computes to      clips  scroll container  user can scroll
visible           visible visible  no     no                no
hidden            hidden hidden    yes    yes               no
clip              clip clip        yes    no                no
scroll            scroll scroll    yes    yes               yes
auto              auto auto        yes    yes               yes
visible hidden    auto hidden      yes    yes               yes
clip auto         hidden auto      yes    yes               yes
clip visible      clip visible     yes    no                no

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

The visible hidden row explains a common surprise: overflow-y: hidden alone, on a box whose overflow-x is still the default visible, makes the horizontal axis auto, so a sideways scrollbar can appear that nobody wrote. overflow applies to block, flex and grid containers, not to inline elements such as a span. On the page itself, a value set on html, or on body when html leaves it visible, is applied to the viewport, which is how html { overflow: hidden; } stops the whole page from scrolling.

hidden or clip: what can still scroll

hidden hides the scrollbars and stops the wheel, the finger and the scroll keys, but the box is still a scroll container: a script may set its scroll position, and moving focus into it may scroll it. clip forbids scrolling through any means. Two boxes 96 px tall each hold a button 200 px below their visible text:

Scrolling a hidden box and a clipped box HTML · hidden-scrolls.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>overflow: hidden can still scroll</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .box { block-size: 96px; margin-block-end: 16px; }
    .box p { margin: 0 0 200px; }
    #hidden { overflow: hidden; }
    #clip { overflow: clip; }
  </style>
</head>
<body>
  <div class="box" id="hidden"><p>Visible text</p><button type="button">A button far below</button></div>
  <div class="box" id="clip"><p>Visible text</p><button type="button">A button far below</button></div>
  <script>
    for (const id of ['hidden', 'clip']) {
      const box = document.getElementById(id);
      const button = box.querySelector('button');
      button.focus();                 // keyboard focus moving to the button
      const focused = box.scrollTop;
      box.scrollTop = 0;
      button.scrollIntoView();        // a script bringing the button into view
      const revealed = box.scrollTop;
      box.scrollTop = 40;             // a script setting the scroll position
      console.log(`${id}: scrollTop ${focused} after focus(), ${revealed} after scrollIntoView(), ${box.scrollTop} after scrollTop = 40`);
    }
  </script>
</body>
</html>
What the script does hidden clip
focus() 152 px 0 px
scrollIntoView() 152 px 0 px
scrollTop = 40 40 px 0 px

The numbers are the box’s scrollTop afterwards. The 152 px scroll brings the button into view, which is good for a keyboard user who tabs into a clipped panel, and confusing when a panel was meant to hide what it holds. Those are Chromium’s and Firefox’s numbers: WebKit focused the button without scrolling the box, leaving it at 0 px, so don’t rely on focus to reveal content either. A clip box shows exactly what is inside its edges, always, and its hidden part can’t be reached at all, so never clip anything a person needs.

Why a sticky header stops sticking

position: sticky keeps an element in view relative to its nearest scroll container. Put a sticky header inside a wrapper with overflow: hidden, often added to stop a sideways scrollbar, and the header sticks to the wrapper, a scroll container that never scrolls, so it moves away with the page as if it were not sticky at all.

A sticky header in a hidden and a clipped wrapper HTML · sticky-hidden-clip.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A sticky header inside overflow: hidden and overflow: clip</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .wrapper { block-size: 1200px; }
    .wrapper header { position: sticky; inset-block-start: 0; block-size: 48px; background: #dde; }
    #hidden { overflow: hidden; }  /* a scroll container: the header sticks to it, and it never scrolls */
    #clip { overflow: clip; }      /* clips too, but is no scroll container: the header sticks to the page */
  </style>
</head>
<body>
  <div class="wrapper" id="hidden"><header>Inside overflow: hidden</header></div>
  <div class="wrapper" id="clip"><header>Inside overflow: clip</header></div>
  <script>
    const topOf = (selector) => document.querySelector(selector).getBoundingClientRect().top;
    // Scroll the page 300px into each wrapper: the wrapper's top is then 300px above the viewport.
    for (const [id, y] of [['hidden', 300], ['clip', 1500]]) {
      window.scrollTo(0, y);
      console.log(`${id}: wrapper top ${topOf(`#${id}`)}px, header top ${topOf(`#${id} header`)}px`);
    }
  </script>
</body>
</html>

The page scrolled 300 px into each wrapper:

Wrapper Its top The header’s top
hidden −300 px −300 px, scrolled away
clip −300 px 0 px, stuck

All three engines agreed. clip is Baseline widely available, so the fix is one word:

Stopping sideways scroll without breaking sticky CSS · sticky-fix.css
/* Before: hidden stops the sideways scroll, but makes .page a scroll container, so the sticky header sticks to
   .page, which never scrolls, and scrolls away with it. */
.page {
  overflow-x: hidden;
}

/* After: clip stops the sideways scroll too, but is no scroll container, so the header sticks to the viewport. */
.page {
  overflow-x: clip;
}
.page > header {
  position: sticky;
  inset-block-start: 0;
}

clip also differs from hidden in what it does not do: it doesn’t start a new formatting context, so the margins of the first and last child still collapse through it, as the margin lab of this module measured. Add display: flow-root when you want both.

Truncating text: one line and several lines

Two recipes cut text to fit, each with an ellipsis. One line needs three declarations together: overflow: hidden, because text-overflow acts only on a box that clips; white-space: nowrap, so the text overflows sideways instead of wrapping; and text-overflow: ellipsis. Several lines use the old -webkit-box layout with -webkit-line-clamp. A product title and two descriptions in a 240 px column, with 24 px lines:

A one-line ellipsis and a three-line clamp HTML · truncate.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>One-line ellipsis and a three-line clamp</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; inline-size: 240px; }
    .one-line { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    .clamp {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 3;
      overflow: hidden;
      margin-block: 16px 0;
    }
  </style>
</head>
<body>
  <p class="one-line" id="title">Stainless steel water bottle, one litre, keeps drinks cold for a day</p>
  <p class="clamp" id="long">A double-walled bottle with a screw cap and a loop for carrying. It keeps drinks cold for 24 hours or hot for 12, and the powder coat does not slip in a wet hand. Dishwasher safe, apart from the cap.</p>
  <p class="clamp" id="short">A one-litre steel bottle.</p>
  <script>
    const title = document.getElementById('title');
    console.log(`one line: ${title.offsetHeight}px tall, text ${title.scrollWidth}px wide in a ${title.clientWidth}px box, so it is cut`);
    for (const id of ['long', 'short']) {
      const p = document.getElementById(id);
      const lines = p.offsetHeight / 24;
      console.log(`clamp, ${id} text: ${p.offsetHeight}px tall, ${lines} line${lines === 1 ? '' : 's'} of 24px`);
    }
  </script>
</body>
</html>
Text Result in all three engines
the title, one line 24 px tall, 455 px of text cut with an ellipsis
the long description 72 px tall: three lines, the last with an ellipsis
the short description 24 px tall: short text keeps its height

The prefixed -webkit-line-clamp is defined in CSS Overflow 4 for compatibility and works in all three engines, as long as it comes with display: -webkit-box and -webkit-box-orient: vertical. The unprefixed line-clamp property, which needs neither, has not shipped in any engine yet, so keep the prefixed form.

Truncated text is still in the page: a screen reader reads all of it, and so does search. People who see the screen only get the first lines, so truncate only text that is repeated or reachable elsewhere, such as a description with a link to the full product page, and never a price, an error or a warning. A title attribute is no substitute, because it does not show on touch screens or for keyboard users. Check the result with text zoomed to 200 % and with longer translations, where more of the text disappears.

Scrollbars that don’t shift the page

Browsers draw two kinds of scrollbar. Classic scrollbars take space from the box. Overlay scrollbars are drawn over the content and take none. Which kind a page gets depends on the system and its settings: phones and most Macs use overlay scrollbars, while many desktop browsers on Windows and Linux use classic ones. When a classic scrollbar appears, the content narrows by its width, and centred content moves sideways by half of it. The same jump happens when a dialog sets overflow: hidden on the page to stop it scrolling, and the scrollbar disappears.

scrollbar-gutter: stable reserves the space of a classic scrollbar even when there is none, so nothing moves. The gutter page measures this on whatever computer opens it:

A centred column with and without scrollbar-gutter HTML · gutter.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>scrollbar-gutter: stable</title>
  <style>
    body { margin: 0; }
    main { inline-size: 300px; margin-inline: auto; block-size: 100px; background: #dde; }
    .tall main { block-size: 2000px; }   /* tall enough for the page to scroll */
    .locked { overflow: hidden; }         /* what a dialog often does to the page while it is open */
    .stable { scrollbar-gutter: stable; }
    .probe { overflow: scroll; inline-size: 100px; block-size: 50px; }
  </style>
</head>
<body>
  <main></main>
  <div class="probe"></div>
  <script>
    const probe = document.querySelector('.probe');
    const bar = probe.offsetWidth - probe.clientWidth; // 0 for overlay scrollbars, which take no space
    console.log(`scrollbars here: ${bar === 0 ? 'overlay, taking no space' : `classic, ${bar}px wide`}`);
    probe.remove();
    const root = document.documentElement;
    const left = () => document.querySelector('main').getBoundingClientRect().left;
    for (const gutter of ['', 'stable']) {
      const at = [];
      for (const page of ['', 'tall', 'tall locked']) {
        root.className = `${gutter} ${page}`;
        at.push(left());
      }
      console.log(`${gutter ? 'scrollbar-gutter: stable' : 'no scrollbar-gutter'}: main starts at ${at.join('px, ')}px (short page, tall page, tall page locked)`);
    }
  </script>
</body>
</html>

Our run had overlay scrollbars in all three engines, so the column started at 250 px in every state and the gutter reserved nothing, which is also what a phone does. With classic scrollbars the numbers change, and the program shows them for a scrollbar 15 px wide; change 15 to the width your system uses:

The jump that scrollbar-gutter removes JavaScript · gutter_shift.mjs
// Where a centred 300px column starts in an 800px window, on a short page, on a page tall enough to scroll, and on
// that page while a dialog sets overflow: hidden, with and without scrollbar-gutter: stable. Overlay scrollbars
// take no space; classic ones take their width from the page. Set the width your system uses and run it again.

const windowWidth = 800;
const column = 300;
const start = (available) => (available - column) / 2;

function columnStarts(scrollbar, stable) {
  const shortPage = stable ? windowWidth - scrollbar : windowWidth;     // the gutter is kept even with no scrollbar
  const tallPage = windowWidth - scrollbar;                             // the scrollbar takes its space
  const locked = stable ? windowWidth - scrollbar : windowWidth;        // overflow: hidden removes the scrollbar
  return [shortPage, tallPage, locked].map(start);
}

for (const [label, scrollbar] of [['overlay scrollbars', 0], ['classic scrollbars, 15px wide', 15]]) {
  for (const stable of [false, true]) {
    const [a, b, c] = columnStarts(scrollbar, stable);
    const name = stable ? 'scrollbar-gutter: stable' : 'no gutter';
    console.log(`${label}, ${name}: ${a}px, ${b}px, ${c}px`);
  }
}

Output

overlay scrollbars, no gutter: 250px, 250px, 250px
overlay scrollbars, scrollbar-gutter: stable: 250px, 250px, 250px
classic scrollbars, 15px wide, no gutter: 250px, 242.5px, 250px
classic scrollbars, 15px wide, scrollbar-gutter: stable: 242.5px, 242.5px, 242.5px

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

Without the gutter the column jumps 7.5 px to the left when the page becomes tall enough to scroll, and 7.5 px back when a dialog locks it. With scrollbar-gutter: stable it stays at 242.5 px. Put it on html: the browser applies it to the viewport from there, but, unlike overflow, never from body. scrollbar-gutter and scrollbar-width are Baseline newly available, and scrollbar-color joined them more recently.

A stable page gutter, and a thin scrollbar for a panel CSS · page-scrollbars.css
/* Reserve the scrollbar's space on the page all the time, so nothing shifts when it appears or when a dialog
   sets overflow: hidden. It goes on html: the browser applies it to the viewport from there, never from body. */
html {
  scrollbar-gutter: stable;
}

/* A thinner scrollbar in a small scrolling panel, in colours that match it (thumb, then track). */
.side-panel {
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #5b6475 #e8eaf0;
}

Styling stops at width and colour on purpose. scrollbar-width: thin asks for a narrower bar, which the browser may ignore if the user has asked for wider scrollbars, and scrollbar-color takes a thumb colour and a track colour, which need enough contrast between them to stay visible. The ::-webkit-scrollbar pseudo-elements in older tutorials are not standard; the Scrollbars specification left them out deliberately, and where the standard scrollbar-color and scrollbar-width are set, they override them.

Custom Scrollbar CSS Generator Try thumb and track colours and a thin width, and copy the standard properties.

Scroll chaining and smooth scrolling

When a scrolling panel reaches its end, the next scroll gesture moves the page behind it, which feels broken in a chat list or a dialog. overscroll-behavior: contain keeps the scroll inside the panel. Chromium and Firefox support it. web-features does not list Safari as supporting it, so it counts as limited availability, although WebKit parses the property; test scroll chaining on a real iPhone before relying on it.

Contained scrolling, and smooth scrolling on request CSS · scroll-behaviour.css
/* A chat list or a dialog body: scrolling past its end does not scroll the page behind it. */
.messages {
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Smooth scrolling for in-page links, only for people who have not asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

scroll-behavior: smooth animates scrolling that a script or an in-page link starts, never the user’s own scrolling. Movement on screen that the user did not make can bring on dizziness and nausea in people with vestibular disorders, so set it only inside @media (prefers-reduced-motion: no-preference), which matches when the user has not asked their system for less motion.

Long words and URLs in narrow containers

A message that contains a long URL is one long “word” with no place to wrap. In a chat row, where the bubble is a flex item next to an avatar, it does more damage than in a plain block, because a flex item does not shrink below its min-content width, and that width is now the whole URL. Four bubbles in a 320 px row:

A long URL in a flex chat bubble HTML · long-url.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A long URL in a chat bubble</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; inline-size: 320px; }
    .message { display: flex; gap: 8px; margin-block-end: 8px; }
    .avatar { flex: none; inline-size: 32px; block-size: 32px; background: #ccd; }
    .bubble { margin: 0; padding: 8px; background: #eef; }
    #break-word .bubble { overflow-wrap: break-word; }
    #anywhere .bubble { overflow-wrap: anywhere; }
    #min-size .bubble { overflow-wrap: break-word; min-inline-size: 0; }
  </style>
</head>
<body>
  <div class="message" id="plain"><div class="avatar"></div><p class="bubble">Track it: https://example.com/orders/track?id=8f14e45fceea167a5a36dedd4bea2543</p></div>
  <div class="message" id="break-word"><div class="avatar"></div><p class="bubble">Track it: https://example.com/orders/track?id=8f14e45fceea167a5a36dedd4bea2543</p></div>
  <div class="message" id="anywhere"><div class="avatar"></div><p class="bubble">Track it: https://example.com/orders/track?id=8f14e45fceea167a5a36dedd4bea2543</p></div>
  <div class="message" id="min-size"><div class="avatar"></div><p class="bubble">Track it: https://example.com/orders/track?id=8f14e45fceea167a5a36dedd4bea2543</p></div>
  <script>
    for (const row of document.querySelectorAll('.message')) {
      const bubble = row.querySelector('.bubble');
      const spill = row.scrollWidth - row.clientWidth;
      console.log(`${row.id}: bubble ${bubble.offsetWidth}px wide, ${spill > 0 ? `${spill}px past the 320px row` : 'inside the 320px row'}`);
    }
  </script>
</body>
</html>
The bubble has Result
nothing 313 px wide, 33 px past the row
break-word 313 px wide, 33 px past the row
anywhere 280 px wide, inside the row
break-word and min-inline-size: 0 280 px wide, inside the row

break-word and anywhere both let the browser break a word that doesn’t fit, but only anywhere counts those breaks when it works out the min-content width, so only anywhere lets the flex item shrink. break-word needs min-inline-size: 0 on the item to do the same. All three engines measured identical widths. For code samples and wide tables, where breaking inside the text would be wrong, make the box scroll with overflow-x: auto instead.

Practise: overflow rules and sticky headers

Exercise · Medium · JavaScript

Work out scroll containers and where a sticky header sticks

A sticky header that stops sticking, or a sideways scrollbar that appears from nowhere, comes from the rules that turn the overflow you write into the values the browser uses. Write two functions in overflow.mjs and export them.

The first function, overflowFacts(x, y = x), takes the values of overflow-x and overflow-y, each one of 'visible', 'hidden', 'clip', 'scroll' and 'auto', and returns { computed, scrollContainer, userScrolls }:

  • computed is the pair the browser computes, as an array [x, y]. hidden, scroll and auto are the scrollable values. When one axis has a scrollable value, visible on the other axis computes to auto and clip to hidden; otherwise the values stay as written.
  • scrollContainer is true when either computed value is scrollable.
  • userScrolls is true when either computed value is scroll or auto: hidden can be scrolled by scripts and focus, but not with a wheel, a finger or the keyboard's scroll keys.

The second, stickyScroller(ancestors), takes the overflow of a sticky element's ancestors, from its parent outwards, each written like the shorthand ('hidden' or 'clip visible', x first), and returns the index of the box the element sticks to: the nearest ancestor that is a scroll container. When none is, it sticks to the viewport: return -1.

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

Starter code · overflow.mjs

/** The computed overflow pair and what follows from it. */
export function overflowFacts(x, y = x) {
  // Replace this line with your code.
  return { computed: [x, y], scrollContainer: false, userScrolls: false };
}

/** The index of the ancestor a sticky element sticks to, or -1 for the viewport. */
export function stickyScroller(ancestors) {
  // Replace this line with your code.
  return -1;
}
The sample tests · overflow.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { overflowFacts, stickyScroller } from './overflow.mjs';

test('one value for both axes', () => {
  assert.deepEqual(overflowFacts('visible'), { computed: ['visible', 'visible'], scrollContainer: false, userScrolls: false });
  assert.deepEqual(overflowFacts('hidden'), { computed: ['hidden', 'hidden'], scrollContainer: true, userScrolls: false });
  assert.deepEqual(overflowFacts('clip'), { computed: ['clip', 'clip'], scrollContainer: false, userScrolls: false });
  assert.deepEqual(overflowFacts('scroll'), { computed: ['scroll', 'scroll'], scrollContainer: true, userScrolls: true });
  assert.deepEqual(overflowFacts('auto'), { computed: ['auto', 'auto'], scrollContainer: true, userScrolls: true });
});

test('visible next to a scrollable value computes to auto', () => {
  assert.deepEqual(overflowFacts('visible', 'hidden'), { computed: ['auto', 'hidden'], scrollContainer: true, userScrolls: true });
  assert.deepEqual(overflowFacts('scroll', 'visible'), { computed: ['scroll', 'auto'], scrollContainer: true, userScrolls: true });
});

test('clip next to a scrollable value computes to hidden', () => {
  assert.deepEqual(overflowFacts('clip', 'auto'), { computed: ['hidden', 'auto'], scrollContainer: true, userScrolls: true });
  assert.deepEqual(overflowFacts('hidden', 'clip'), { computed: ['hidden', 'hidden'], scrollContainer: true, userScrolls: false });
});

test('visible and clip together stay as written', () => {
  assert.deepEqual(overflowFacts('clip', 'visible'), { computed: ['clip', 'visible'], scrollContainer: false, userScrolls: false });
  assert.deepEqual(overflowFacts('visible', 'clip'), { computed: ['visible', 'clip'], scrollContainer: false, userScrolls: false });
});

test('a sticky header sticks to the nearest scroll container', () => {
  assert.equal(stickyScroller(['visible', 'hidden', 'auto']), 1);
  assert.equal(stickyScroller(['auto']), 0);
  assert.equal(stickyScroller(['clip visible', 'visible hidden']), 1);
});

test('with no scroll container among the ancestors, it sticks to the viewport', () => {
  assert.equal(stickyScroller(['visible', 'clip', 'visible clip']), -1);
  assert.equal(stickyScroller([]), -1);
});
A hint

Keep the three scrollable values in a Set. For each axis, look at the other axis: only when the other value is in the set do visible and clip change. For stickyScroller, split each ancestor's text on the space (a single value stands for both axes) and return the first index whose overflowFacts says it is a scroll container.

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.

Interview questions

Fresher · What is the difference between overflow: hidden and overflow: clip?

Both clip content at the box’s edge and show no scrollbars. hidden still makes the box a scroll container: scripts can scroll it with scrollTop or scrollIntoView(), focus moving to an element inside can scroll it, a sticky descendant sticks to it, and it starts a new block formatting context, so its children’s margins stay inside. clip forbids scrolling completely, is not a scroll container and does not start a formatting context, so a sticky header inside it sticks to the next scroll container out, usually the page. That is why overflow-x: clip is the safe way to stop sideways scrolling on a wrapper that holds a sticky header, where hidden breaks the header. clip also lets you clip one axis while the other stays visible.

Key takeaways

  • visible spills, hidden clips but can still be scrolled by scripts and focus, clip clips and never scrolls, scroll always shows scrollbars and auto shows them when needed.
  • hidden, scroll and auto make a scroll container; next to one of them, visible computes to auto and clip to hidden.
  • A sticky element sticks to its nearest scroll container, so overflow: hidden on a wrapper breaks it; use overflow: clip.
  • One-line truncation needs overflow: hidden, white-space: nowrap and text-overflow: ellipsis; several lines need display: -webkit-box, -webkit-box-orient: vertical and -webkit-line-clamp.
  • scrollbar-gutter: stable on html stops classic scrollbars from shifting the page; style scrollbars only with scrollbar-width and scrollbar-color.
  • overflow-wrap: anywhere lets a flex item with a long URL shrink; with break-word, also set min-inline-size: 0.

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 wrapper has overflow: hidden to stop a sideways scrollbar, and the sticky header inside it no longer sticks when the page scrolls. What fixes it without bringing the scrollbar back?

    Choose one answer.

    Show the answer to question 1

    Answer: overflow-x: clip on the wrapper instead

    hidden makes the wrapper a scroll container, so the header sticks to the wrapper, which never scrolls, and moves away with it: in the lesson's run its top was −300 px after the page scrolled 300 px. clip clips the same way but is not a scroll container, so the header sticks to the viewport and stayed at 0 px. auto would bring the scrollbar back.

  2. Question 2 of 6 You write overflow-x: visible; overflow-y: hidden;. What does the browser compute?

    Choose one answer.

    Show the answer to question 2

    Answer: overflow-x: auto; overflow-y: hidden

    A box can't let content spill out on one axis while it is a scroll container on the other, so visible next to a scrollable value computes to auto. That is why this pair can show a horizontal scrollbar you never asked for. clip next to a scrollable value computes to hidden; visible with clip stays as written.

  3. Question 3 of 6 Which declarations does a one-line truncation with an ellipsis need on the text's block?

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • overflow: hidden
    • white-space: nowrap
    • text-overflow: ellipsis

    text-overflow only acts on content that overflows a box whose overflow is not visible, and text only overflows sideways when it can't wrap, so all three are needed. display: -webkit-box with -webkit-line-clamp is the separate recipe for clamping several lines.

  4. Question 4 of 6 A description has line-height: 22px and a three-line clamp with -webkit-line-clamp: 3. How tall is it when the text is long enough for six lines?

    Type a number.

    Show the answer to question 4

    Answer: 66 px

    The clamp keeps three lines: 3 × 22 = 66 px. Shorter text keeps its own height; in the lesson's run a one-line description stayed one line tall, 24 px, inside the same clamp.

  5. Question 5 of 6 Where should scrollbar-gutter: stable go so that the page does not shift sideways when a dialog sets the page's overflow to hidden?

    Choose one answer.

    Show the answer to question 5

    Answer: On the html element

    Set on the root element, scrollbar-gutter applies to the viewport. Unlike overflow, it is never taken from body. With classic scrollbars the gutter then stays reserved while the page is locked, so a centred column does not jump; with overlay scrollbars nothing takes space and nothing moves.

  6. Question 6 of 6 A chat bubble is a flex item, and a long URL in it pushes the row 33 px past its container. Which change alone keeps the bubble inside?

    Choose one answer.

    Show the answer to question 6

    Answer: overflow-wrap: anywhere on the bubble

    The flex item can't shrink below its min-content width, which is the width of the URL. anywhere lets the URL break and counts those breaks in the min-content width, so the bubble shrinks to 280 px. break-word also breaks the URL, but its breaks do not count for min-content, so it overflows exactly as before unless you also add min-inline-size: 0, as all three engines measured.

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.