Mert Özen Further With Every Line
Frontend CSS Properties: A Hands-On Guide — #11

Overflow and Scroll Behavior: overflow: clip, scroll-snap, scroll-padding, overscroll-behavior, touch-action and Scrollbar Styling

Mert Özen Nov 15, 2026 17 min 15 views
Overflow and Scroll Behavior: overflow: clip, scroll-snap, scroll-padding, overscroll-behavior, touch-action and Scrollbar Styling

overflow: hidden still makes the box a scroll container, overflow: clip does not; for a scrolling strip scroll-snap-type with scroll-snap-align is enough, and scroll-padding keeps anchor targets out from under a sticky bar. The post measures Bookstore's Recommended strip, overscroll-behavior and scroll lock in the cart drawer, touch-action, and scrollbar-gutter/scrollbar-color in Chromium and WebKit.

Version note: This post follows the CSS Overflow Level 3, CSS Scroll Snap Level 1, CSS Overscroll Behavior Level 1, CSS Scrollbars Styling Level 1 and Pointer Events (touch-action) specifications and the November 2026 state of Baseline (web-features 3.46.0); the examples were run in Chromium 158 and WebKit 27.2 (Playwright 1.65), and Firefox support comes from web-features and MDN browser-compat-data 8.1.9.

Guess First

For Bookstore, the online bookstore used as the shared example across this site's series, this post solves four scrolling jobs: the "Recommended" strip on the book page should stop on whole covers, the sticky filter bar in the catalog should not cover anchor targets, the page behind the cart should not move while the cart is open, and scrollbars should not make the layout jump. First, two boxes:

.frame { inline-size: 200px; block-size: 60px; border: 1px solid #d1d5db; white-space: nowrap; }
.frame a { margin-inline-start: 400px; }

/* 1) hidden still makes a scroll container, clip does not */
#hidden-box { overflow: hidden; }
#clip-box { overflow: clip; }
<div class="frame" id="hidden-box">Dune, Emma <a href="#" id="hidden-link">Middlemarch</a></div>
<div class="frame" id="clip-box">Dune, Emma <a href="#" id="clip-link">Middlemarch</a></div>

Both boxes are 200 pixels wide and the "Middlemarch" link sits 400 pixels to the right, outside the box. When a user tabs to that link, which box scrolls its content to show it? Neither has a scrollbar, and both cut off what overflows.

overflow: hidden, clip, auto and scroll

The measurement focuses the link in each box of the lab page and reads scrollLeft; then it tries to scroll the box by 30 pixels from JavaScript:

node scripts/11-scroll.mjs
--- chromium
overflow: hidden scrollLeft after focus 390, after scrollLeft = 30: 30
overflow: clip   scrollLeft after focus 0, after scrollLeft = 30: 0
--- webkit
overflow: hidden scrollLeft after focus 390, after scrollLeft = 30: 30
overflow: clip   scrollLeft after focus 0, after scrollLeft = 30: 0

The overflow: hidden box scrolled 390 pixels to show the focused link and accepted the scrollLeft = 30 from JavaScript as well. hidden only removes the scrollbar and wheel scrolling by the user; the box is still a scroll container. Focus, scrollIntoView(), find in page and anchor links can scroll it. overflow: clip really cuts the overflow off: the box is not a scroll container and scrollLeft stays 0. The answer to the opening question: only the hidden box scrolls, the same in both engines.

The difference matters most for sticky elements: position: sticky sticks to the nearest scroll container, so an overflow: hidden ancestor in between breaks it. We measured this, and how overflow-x: hidden turns the other axis into auto, in post 04 of the site's CSS series (CSS3: Flexbox, Grid and Modern CSS). clip has one more advantage: you can write clip on one axis and visible on the other, while hidden turns the other axis into auto in that combination. overflow-clip-margin, which moves the clipping edge outwards, is not Baseline.

The difference between auto and scroll is when the scrollbar is drawn: auto draws it only when the content overflows, scroll draws it even when the content fits, which shows an empty track on systems with classic scrollbars. In Bookstore the cover frame used overflow: hidden to cut off the image that grows on hover; nobody ever needs to scroll that frame, so the right value is clip:

.book-card .cover {
    margin-block-end: 0.75rem;
    border-radius: 0.25rem;
    /* the scaled image stays inside the frame; clip: the frame is no scroll container */
    overflow: clip;
}

Before adding snapping to the strip I measured it at phone size (480×800) and got a surprise:

node scripts/11-pages.mjs
--- chromium
strip without flex: none: cover 45.5px, scrollWidth 417, clientWidth 417
strip with flex: none:    cover 160px, scrollWidth 1104, clientWidth 417
strip: scroll-snap-type x mandatory, scroll-padding-inline 32px, overscroll-behavior-x contain
strip: scrollTo(100) ends at 176
strip: focus on the 6th cover → scrollLeft 687 (max 687), cover starts 225px from the strip edge
--- webkit
strip without flex: none: cover 48px, scrollWidth 432, clientWidth 432
strip with flex: none:    cover 160px, scrollWidth 1104, clientWidth 432
strip: scroll-snap-type x mandatory, scroll-padding-inline 32px, overscroll-behavior-x contain
strip: scrollTo(100) ends at 176
strip: focus on the 6th cover → scrollLeft 672 (max 672), cover starts 240px from the strip edge

The first line shows that without flex: none the covers shrink from 160 to 45.5 pixels, 48 in WebKit, and the strip does not scroll at all: scrollWidth equals clientWidth. The cause is the img reset we added in this series' images post: an image with max-inline-size: 100% counts as having no minimum width. A flex item's automatic minimum, min-width: auto from post 05 of the CSS3 series, comes from its content, so nothing protects the li and the default flex-shrink: 1 squeezes the covers. Items in a strip that is meant to scroll must not shrink: flex: none. After the fix the strip is 1104 pixels wide and scrolls.

Now the snapping. scroll-snap-type goes on the container, scroll-snap-align on the items:

.book-strip {
    display: flex;
    gap: 1rem;
    margin: 0;
    /* the first and last cover start outside the faded edges */
    padding: 0 2rem;
    list-style: none;
    overflow-x: auto;
    /* snap to whole covers; the snap line starts after the faded edge */
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 2rem;
    /* a sideways swipe at the end does not become "back" in the browser history */
    overscroll-behavior-inline: contain;
    /* faded edges: a hint that the row scrolls sideways */
    mask-image: linear-gradient(to right, transparent, #000000 2rem, #000000 calc(100% - 2rem), transparent);
}
.book-strip li {
    /* do not shrink: an img with a percentage max-inline-size gives the item no minimum width */
    flex: none;
    scroll-snap-align: start;
}
.book-strip img { inline-size: 10rem; border-radius: 0.25rem; }

x mandatory makes every horizontal scroll end on a snap position; proximity snaps only when the scroll ends near one. scroll-snap-align: start aligns the start of a cover with the snap line. By default the snap line is the edge of the container, but the edges of this strip were faded with mask-image in the series' clip-path and mask post. scroll-padding-inline: 2rem moves the snap line 32 pixels inwards, so a cover never stops behind the faded part.

In the measurement scrollTo(100) ended at 176 in both engines: 160 pixels of cover plus a 16 pixel gap, so the second cover sits exactly on the snap line. The keyboard follows the same rule: tabbing to the sixth cover scrolled the strip to its end, 687 in Chromium and 672 in WebKit, both the maximum. The two engines report a different clientWidth because Chromium reserves 15 pixels on the page for a scrollbar; the scrollbar section explains why.

Two more properties belong here. scroll-snap-stop: always on an item means a fast scroll cannot skip over it, which suits a gallery that moves one card at a time. overscroll-behavior-inline: contain stops the horizontal scroll from passing to the page at the end of the strip; according to the specification it also stops a sideways swipe on a trackpad or touch screen from going "back" in the browser history. I did not test that last behavior with a real trackpad or phone.

scroll-padding, scroll-margin and scroll-behavior

The filter bar in the catalog is sticky and sticks to the top of the page. Clicking "Reading list" in the header brings the #reading-list heading to the very top of the page, right under the bar. First the problem in the lab, with a 50 pixel sticky header:

/* 2) scroll-margin: the target keeps its distance from the sticky header */
#feed { block-size: 300px; overflow-y: auto; border: 1px solid #d1d5db; }
#feed header { position: sticky; inset-block-start: 0; block-size: 50px; background: #e5e7eb; }
#feed h2 { margin: 0; padding-block-start: 400px; font-size: 1rem; }
#target-margin { scroll-margin-block-start: 60px; }
--- chromium
target-plain  lands 0px below the top of the feed (header is 50px)
target-margin lands 60px below the top of the feed (header is 50px)
--- webkit
target-plain  lands 0px below the top of the feed (header is 50px)
target-margin lands 60px below the top of the feed (header is 50px)

The plain heading brought into view with scrollIntoView() landed at pixel 0, under the header. The heading with scroll-margin-block-start: 60px landed at pixel 60, 10 pixels below the header. scroll-margin goes on the target element and affects only that element. If you want one value for every target, you put scroll-padding on the scroll container, which is html for page scrolling. In Bookstore the value lives in the file of the filter bar, which causes the problem, and :has() limits it to pages that have a filter bar; the selector side is in post 02 of the CSS3 series:

/* anchor targets stop below the sticky bar, not under it */
html:has(.filter-bar) { scroll-padding-block-start: 4rem; }

In base.css we also smooth the jump to anchors. scroll-behavior: smooth applies to anchor links and to scrollTo() calls without a behavior. To avoid animating for users who asked for less motion, it sits in the no-preference branch of prefers-reduced-motion; the reasoning is in post 13 of the CSS3 series. We come back to the scrollbar-gutter line in the cart section:

html {
    /* room for the scrollbar is kept even when the page cannot scroll (no shift when a modal locks it) */
    scrollbar-gutter: stable;
}
/* smooth jumps to anchors, only for people who did not ask for less motion */
@media (prefers-reduced-motion: no-preference) {
    html { scroll-behavior: smooth; }
}
--- chromium
reading list (no-preference): animated, filter bar bottom 52px, heading top 64px
reading list (reduce       ): jumped, filter bar bottom 52px, heading top 64px
--- webkit
reading list (no-preference): animated, filter bar bottom 52px, heading top 64px
reading list (reduce       ): jumped, filter bar bottom 52px, heading top 64px

"Reading list" is the last section of the page and could not reach the top, so the measurement adds one screen of space below main. The result is the same in both engines: the bottom of the bar is at pixel 52 and the heading at pixel 64, which is 4rem. With the default setting the jump is animated; on a page opened with reducedMotion: 'reduce' the heading arrives in one step. The bar's height can change with the font and line height, so we kept 4rem a little above the measured 52 pixels.

overscroll-behavior and Scroll Lock: the Cart Drawer

When a scrolling box reaches its end, the browser hands the scroll to the next scroll container up and finally to the page. This is scroll chaining, and it is why the page behind a drawer moves while you scroll the drawer's list. The lab has a fixed panel at the top right; the panel is scrolled to its end, then the wheel turns 300 pixels over it:

/* 4) scroll chaining: what happens when the panel reaches its end? */
#panel { position: fixed; inset-block-start: 20px; inset-inline-end: 20px; inline-size: 300px; block-size: 300px;
    overflow-y: auto; border: 1px solid #d1d5db; background: #f9fafb; }
#panel.contain { overscroll-behavior: contain; }
#panel .content { block-size: 600px; }
#panel.short .content { block-size: 100px; }
--- chromium
panel long          page scrollY after wheel: 300
panel long contain  page scrollY after wheel: 0
panel short         page scrollY after wheel: 300
panel short contain page scrollY after wheel: 0
--- webkit
panel long          page scrollY after wheel: 300
panel long contain  page scrollY after wheel: 0
panel short         page scrollY after wheel: 300
panel short contain page scrollY after wheel: 300

With long content, overscroll-behavior: contain does its job in both engines: the page stays put. With short content, where the panel is a scroll container with nothing to scroll, the engines differ: Chromium still holds the page, WebKit scrolls it by 300 pixels. The browser-compat-data note says exactly this: in Safari contain has no effect on scroll containers without scrollable overflow; Chrome fixed this in 144 and Firefox in 150. That is why web-features counts overscroll-behavior as not Baseline. The basic use has worked everywhere since Chrome 63, Firefox 59 and Safari 16, and it is safe for stopping pull-to-refresh on Android and scroll chaining to the page.

The Bookstore cart is a modal <dialog> opened by the button in the catalog header; the element itself is covered in post 06 of the site's HTML series (HTML5 and Semantic HTML). Only the list of items scrolls:

<dialog class="cart-drawer" id="cart-drawer" aria-labelledby="cart-title">
    <header><h2 id="cart-title">Your cart</h2></header>
    <ul class="cart-items">
        <li><img src="img/cover-dune.webp" alt="" width="240" height="360" loading="lazy"> Dune <span class="price">₺420.00</span></li>
        <li><img src="img/cover-middlemarch.webp" alt="" width="240" height="360" loading="lazy"> Middlemarch <span class="price">₺385.50</span></li>
        <li><img src="img/cover-emma.webp" alt="" width="240" height="360" loading="lazy"> Emma <span class="price">₺240.00</span></li>
        <li><img src="img/cover-persuasion.webp" alt="" width="240" height="360" loading="lazy"> Persuasion <span class="price">₺255.00</span></li>
        <li><img src="img/cover-jane-eyre.webp" alt="" width="240" height="360" loading="lazy"> Jane Eyre <span class="price">₺298.00</span></li>
        <li><img src="img/cover-frankenstein.webp" alt="" width="240" height="360" loading="lazy"> Frankenstein <span class="price">₺210.00</span></li>
        <li><img src="img/cover-hobbit.webp" alt="" width="240" height="360" loading="lazy"> The Hobbit <span class="price">₺310.00</span></li>
    </ul>
    <footer>
        <p class="cart-total">Total ₺2,118.50</p>
        <form method="dialog"><button type="submit">Close</button></form>
    </footer>
</dialog>
/* the cart opens as a modal dialog docked to the right edge */
.cart-drawer {
    inset-block: 0;
    inset-inline: auto 0;
    inline-size: min(24rem, 100%);
    max-inline-size: none;
    block-size: 100dvh;
    max-block-size: none;
    margin: 0;
    padding: 0;
    border: 0;
    box-shadow: var(--shadow-lg);
}
.cart-drawer[open] { display: flex; flex-direction: column; }
.cart-drawer header,
.cart-drawer footer { padding: 1rem 1.5rem; }
.cart-drawer h2 { margin: 0; }

/* only the list scrolls; header and footer stay in place */
.cart-items {
    flex: 1;
    min-block-size: 0;
    margin: 0;
    padding: 0 1.5rem;
    list-style: none;
    overflow-y: auto;
    /* the end of the list does not hand the scroll over to the page */
    overscroll-behavior: contain;
}
.cart-items li { display: flex; gap: 1rem; align-items: center; padding-block: 0.5rem; }
.cart-items img { inline-size: 4rem; border-radius: 0.25rem; }
.cart-items .price { margin-inline-start: auto; font-weight: 700; }

/* scroll lock: while the drawer is open the page behind it does not scroll */
html:has(.cart-drawer[open]) { overflow: hidden; }

A modal dialog makes the page behind it inert, but it does not stop it from scrolling: turning the wheel over the backdrop scrolls the page. So there are two measures. overscroll-behavior: contain for the list, and overflow: hidden on html while the dialog is open, the scroll lock. The measurement compares three cases with the page opened with classic scrollbars, as on Windows:

--- chromium
drawer (as shipped): main 465px → 465px, scrollY after wheel on backdrop 0, list 624px of 784px, overscroll-behavior contain
drawer (no gutter ): main 465px → 480px, scrollY after wheel on backdrop 0, list 624px of 784px, overscroll-behavior contain
drawer (no lock   ): main 465px → 465px, scrollY after wheel on backdrop 400, list 624px of 784px, overscroll-behavior contain
--- webkit
drawer (as shipped): main 480px → 480px, scrollY after wheel on backdrop 0, list 624px of 784px, overscroll-behavior contain
drawer (no gutter ): main 480px → 480px, scrollY after wheel on backdrop 0, list 624px of 784px, overscroll-behavior contain
drawer (no lock   ): main 480px → 480px, scrollY after wheel on backdrop 400, list 624px of 784px, overscroll-behavior contain

As written, the page does not scroll and main stays at 465 pixels. Without scrollbar-gutter, main grows from 465 to 480 the moment the lock removes the scrollbar: all the content behind moves 15 pixels to the right. Without the lock, the wheel over the backdrop scrolls the page by 400 pixels. The list holds 784 pixels of content in 624 pixels of space, so it is a scroll container where contain really has work to do. WebKit on macOS uses overlay scrollbars that appear over the content and take no space, so the width is always 480 there and you cannot see the shift.

A warning for iOS Safari: for years developers reported that overflow: hidden on html or body does not fully lock the page on an iPhone. WebKit bug 153852 was closed as fixed, but similar complaints kept coming for later versions. The common workaround is to make body position: fixed when opening, save the scroll position, and restore it when closing. I did not try this on a real iOS device; test the drawer on the iOS versions you target before you ship it.

touch-action

touch-action says which finger movements that start on an element the browser handles itself; the rest is left to JavaScript. I cannot produce touch gestures in Playwright's WebKit, so this measurement runs only in Chromium, with the touch scroll command of the DevTools protocol:

await page.locator('#swipe').evaluate((el, t) => { el.style.touchAction = t; }, touchAction);
const b = await page.locator('#swipe').boundingBox();
const cdp = await page.context().newCDPSession(page);
await cdp.send('Input.synthesizeScrollGesture',
    { x: b.x + 300, y: b.y + 40, xDistance, yDistance, gestureSourceType: 'touch', speed: 800 });
--- chromium, touch
touch-action: auto         sideways swipe moves the strip: yes, upward swipe moves the page: yes
touch-action: pan-y        sideways swipe moves the strip: no , upward swipe moves the page: yes
touch-action: pan-x        sideways swipe moves the strip: yes, upward swipe moves the page: no
touch-action: manipulation sideways swipe moves the strip: yes, upward swipe moves the page: yes

The horizontal strip with pan-y does not move with a sideways finger movement, but an upward movement scrolls the page. That is the right value for a slider or a swipe-to-delete list whose sideways drag you write yourself: vertical page scrolling keeps working and the horizontal movement belongs to your code. pan-x does the opposite. manipulation allows panning and pinch zoom and only turns off double-tap zoom; the browser no longer waits for a second tap, so the tap delay on buttons goes away. We use it on the quantity buttons in the next post. touch-action: none turns everything off and is right only where you handle every movement yourself, such as a drawing area or a map.

Scrollbar Styling: scrollbar-gutter, scrollbar-width and scrollbar-color

Scrollbars come in two kinds: on Windows and most Linux desktops the classic scrollbar takes space, on macOS and mobile the overlay scrollbar appears over the content. Playwright's headless Chromium hides scrollbars by default; for the measurements in this post I added a scrollbars option to browser.mjs that turns this off, so Chromium draws classic scrollbars as on Windows:

/* 3) scrollbars */
.bar { inline-size: 200px; block-size: 80px; overflow: auto; border: 1px solid #d1d5db; display: inline-block; }
.bar .tall { block-size: 300px; }
#gutter-short { scrollbar-gutter: stable; }
#thin-long { scrollbar-width: thin; }
#color-long { scrollbar-color: #b91c1c #fde68a; }
#legacy::-webkit-scrollbar, #both::-webkit-scrollbar { inline-size: 8px; }
#legacy::-webkit-scrollbar-thumb, #both::-webkit-scrollbar-thumb { background: #15803d; }
#both { scrollbar-color: #b91c1c #fde68a; }
--- chromium
auto-short    scrollbar  0px  -
auto-long     scrollbar 15px  rgb(250, 250, 250) + rgb(193, 193, 193)
gutter-short  scrollbar 15px  rgb(255, 255, 255)
thin-long     scrollbar 11px  rgb(250, 250, 250) + rgb(193, 193, 193)
color-long    scrollbar 15px  rgb(253, 230, 138) + rgb(185, 28, 28)
legacy        scrollbar  8px  rgb(255, 255, 255) + rgb(21, 128, 61)
both          scrollbar 15px  rgb(253, 230, 138) + rgb(185, 28, 28)
--- webkit
auto-short    scrollbar  0px  -
auto-long     scrollbar  0px  -
gutter-short  scrollbar  0px  -
thin-long     scrollbar  0px  -
color-long    scrollbar  0px  -
legacy        scrollbar  8px  rgb(255, 255, 255) + rgb(21, 128, 61)
both          scrollbar  0px  -

In Chromium the scrollbar of an overflowing box is 15 pixels. The box that does not overflow has no scrollbar and its content is 15 pixels wider; when its content grows, every line wraps again. scrollbar-gutter: stable reserves those 15 pixels even when nothing overflows, which is why we put it on html in Bookstore: the page did not move when the drawer's lock removed the scrollbar. scrollbar-width: thin brought the scrollbar down to 11 pixels, and scrollbar-color painted the thumb, the part you drag, red and the track yellow, in that order. These two properties offer only two colors and three widths, auto, thin and none; the limit is deliberate.

The ::-webkit-scrollbar pseudo-elements you see in older code are not standard, but they still work in Chromium and WebKit: #legacy is 8 pixels with a green thumb. #both has both, and the standard property wins: 15 pixels, red and yellow in Chromium. Chromium did not apply the ::-webkit-scrollbar rules on the element with scrollbar-color; that is why the old scrollbar styles of many sites changed at once when Chrome 121 shipped the standard properties. If you want to keep the old styling, put it inside @supports not (scrollbar-color: auto); since Safari supports scrollbar-color from 26.2, that block runs almost nowhere today. WebKit's overlay scrollbar on macOS measured 0 in every width check.

In Production

Check scrolling behavior on Windows, with classic scrollbars. On a page built on macOS and opened on Windows, the two most common problems are a layout that jumps 15 pixels when content changes and a section with 100vw width that adds a horizontal scrollbar, because 100vw includes the width of the scrollbar. scrollbar-gutter: stable solves the first, 100% or inline-size: auto the second.

If items are added to a snapping strip later, or images load and change size, the specification asks the browser to snap the position again; that is one more reason to give the strip's images width and height, as post 08 of the HTML5 series explains. Do not use mandatory in a container whose items can be larger than the viewport: the content in the middle of such an item may become unreachable. proximity is safer there.

A bug that leaves the scroll lock on, for example a dialog removed from the DOM without close, makes the page impossible to scroll. Tying the lock to the dialog's own state with :has(.cart-drawer[open]), as here, instead of a class set from JavaScript prevents that bug. Animations driven by scrolling are not the topic of this post but of post 14 of the CSS3 series.

Common Pitfalls

First, using overflow: hidden to cut off overflow and forgetting that focus and anchors can still scroll the content. If scrolling is never wanted, write clip.

Second, items shrinking in a scrolling flex strip. Images with a percentage max-inline-size give the item no minimum width; give the items flex: none.

Third, putting overflow: hidden on body and assuming the scroll lock works everywhere. It can leak on iOS Safari, and on systems with classic scrollbars it moves the page by 15 pixels without scrollbar-gutter.

Fourth, hiding the scrollbar completely with scrollbar-width: none. Without another hint that an area scrolls, such as a half-visible cover or a faded edge, users do not notice there is more.

Fifth, expecting overscroll-behavior: contain to hold the page for a panel with short content too. In Safari it has no effect on a scroll container without overflow; add a lock if you need one.

Baseline Status

node scripts/baseline.mjs overflow-clip scroll-snap scroll-behavior overscroll-behavior touch-action scrollbar-gutter scrollbar-width scrollbar-color
overflow-clip              widely 2025-03-12  Chrome 90, Firefox 81, Safari 16, iOS 16
scroll-snap                widely 2022-07-15  Chrome 69, Firefox 68, Safari 11, iOS 11
scroll-behavior            widely 2024-09-14  Chrome 61, Firefox 36, Safari 15.4, iOS 15.4
overscroll-behavior        not Baseline       Chrome 144, Firefox 150, Safari -, iOS -
touch-action               widely 2022-03-19  Chrome 36, Firefox 52, Safari 13, iOS 9.3
scrollbar-gutter           newly 2024-12-11   Chrome 94, Firefox 97, Safari 18.2, iOS 18.2
scrollbar-width            newly 2024-12-11   Chrome 121, Firefox 64, Safari 18.2, iOS 18.2
scrollbar-color            newly 2025-12-12   Chrome 121, Firefox 64, Safari 26.2, iOS 26.2
FeatureStatusNote
overflow: clipWidely available (2025-03)overflow-clip-margin is not Baseline
Scroll snap, scroll-padding, scroll-marginWidely availablescroll-snap-stop since 2022-07
scroll-behaviorWidely available (2024-09)Tie it to reduced motion
overscroll-behaviorNot BaselineBasic use works everywhere; no effect in Safari on containers without overflow
touch-actionWidely availableDirectional values such as pan-up/pan-left only in Chromium
scrollbar-gutter, scrollbar-widthNewly available (2024-12)No effect with overlay scrollbars
scrollbar-colorNewly available (2025-12)Safari 26.2; overrides ::-webkit-scrollbar

A Small Experiment

★☆☆ Change mandatory to proximity on the strip and see where scrollTo(100) ends in 11-pages.mjs; then try 150.

★★☆ Remove scroll-padding-block-start from the catalog and get the same result with scroll-margin-block-start on the reading list heading only; write down when the two approaches give different results, for example when the page has a second target.

★★★ Reduce the cart to two books and apply the lab's panel measurement to the drawer: with the lock off, does the page scroll in WebKit when you turn the wheel over the list? Then propose a measure that can replace contain there and measure it in both engines.

In the next post we move on to form controls: accent-color, a custom checkbox with appearance: none, a growing textarea with field-sizing, cursor, pointer-events and user-select; Bookstore's checkout form gets quantity buttons and an order note.