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.
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 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.
- visible: content spills out of the box, nothing is clipped and nothing scrolls.
- 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.
- 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.
- 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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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:
<!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.
<!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:
/* 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:
<!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:
<!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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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.
/* 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.
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.
/* 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:
<!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 }:
computedis the pair the browser computes, as an array[x, y].hidden,scrollandautoare the scrollable values. When one axis has a scrollable value,visibleon the other axis computes toautoandcliptohidden; otherwise the values stay as written.scrollContaineristruewhen either computed value is scrollable.userScrollsistruewhen either computed value isscrollorauto:hiddencan 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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
visiblespills,hiddenclips but can still be scrolled by scripts and focus,clipclips and never scrolls,scrollalways shows scrollbars andautoshows them when needed.hidden,scrollandautomake a scroll container; next to one of them,visiblecomputes toautoandcliptohidden.- A sticky element sticks to its nearest scroll container, so
overflow: hiddenon a wrapper breaks it; useoverflow: clip. - One-line truncation needs
overflow: hidden,white-space: nowrapandtext-overflow: ellipsis; several lines needdisplay: -webkit-box,-webkit-box-orient: verticaland-webkit-line-clamp. scrollbar-gutter: stableonhtmlstops classic scrollbars from shifting the page; style scrollbars only withscrollbar-widthandscrollbar-color.overflow-wrap: anywherelets a flex item with a long URL shrink; withbreak-word, also setmin-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.
References
- CSS Overflow Module Level 3: the overflow properties and scroll containers (W3C CSS Working Group)
- CSS Overflow Module Level 3: overflow viewport propagation (W3C CSS Working Group)
- CSS Overflow Module Level 3: scrollbar-gutter (W3C CSS Working Group)
- CSS Overflow Module Level 3: text-overflow (W3C CSS Working Group)
- CSS Overflow Module Level 4: -webkit-line-clamp (W3C CSS Working Group)
- CSS Scrollbars Styling Module Level 1 (W3C CSS Working Group)
- CSS Positioned Layout Module Level 3: sticky positioning (W3C CSS Working Group)
- CSS Overscroll Behavior Module Level 1 (W3C CSS Working Group)
- Media Queries Level 5: prefers-reduced-motion (W3C CSS Working Group)
- CSS Text Module Level 3: overflow-wrap (W3C CSS Working Group)
- CSSOM View Module: scrollTop and scrollIntoView() (W3C CSS Working Group)
- HTML Standard: the title attribute (WHATWG)
- Web Content Accessibility Guidelines (WCAG) 2.2: Resize Text (W3C)
- Understanding WCAG 2.2: Animation from Interactions (W3C)
- line-clamp and the prefixed -webkit-line-clamp (MDN Web Docs) (Mozilla)
- ::-webkit-scrollbar (MDN Web Docs) (Mozilla)
- Web features explorer: overscroll-behavior (W3C WebDX Community Group)
- Web features explorer: scrollbar-gutter (W3C WebDX Community Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress