Mert Özen Her Satırda Daha İleri
Frontend CSS Property'leri: Uygulamalı Rehber — #11

Overflow ve Scroll Davranışı: overflow: clip, scroll-snap, scroll-padding, overscroll-behavior, touch-action ve Scrollbar Stilleri

Mert Özen 15 Kas 2026 17 dk 14 görüntülenme
Overflow ve Scroll Davranışı: overflow: clip, scroll-snap, scroll-padding, overscroll-behavior, touch-action ve Scrollbar Stilleri

overflow: hidden kutuyu yine scroll container yapar, overflow: clip yapmaz; kaydırılan bir şerit için scroll-snap-type ile scroll-snap-align, sticky çubuğun altında kalan anchor hedefi için scroll-padding yeterli. Yazı Bookstore'un Recommended şeridini, sepet drawer'ındaki overscroll-behavior ve scroll lock'u, touch-action'ı ve scrollbar-gutter/scrollbar-color'ı Chromium ve WebKit ölçümleriyle anlatır.

Sürüm notu: Bu yazı CSS Overflow Level 3, CSS Scroll Snap Level 1, CSS Overscroll Behavior Level 1, CSS Scrollbars Styling Level 1 ve Pointer Events (touch-action) spesifikasyonlarına ve Baseline'ın Kasım 2026 durumuna (web-features 3.46.0) göre yazıldı; örnekler Chromium 158 ve WebKit 27.2 (Playwright 1.65) ile denenmiştir, Firefox desteği web-features ve MDN'in (Mozilla'nın web dokümantasyon projesi) browser-compat-data 8.1.9 verisinden alınmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için bu yazıda kaydırmayla ilgili dört işi çözüyoruz: kitap detayındaki "Recommended" şeridi kapaklara otursun, katalogdaki sticky filtre çubuğu anchor hedeflerini örtmesin, sepet açılınca arkadaki sayfa kaymasın ve scrollbar'lar sayfayı oynatmasın. Önce iki kutu:

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

/* 1) hidden yine scroll container oluşturur, clip oluşturmaz */
#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>

İki kutu da 200 piksel genişliğinde ve "Middlemarch" linki 400 piksel sağda, yani kutunun dışında. Kullanıcı Tab ile bu linke geldiğinde hangi kutunun içeriği kayıp linki gösterir? İkisinde de scrollbar yok, ikisi de taşanı kesiyor.

overflow: hidden, clip, auto ve scroll

Ölçüm, lab sayfasındaki iki kutuya focus verip scrollLeft'i okuyor; sonra JavaScript ile kutuyu 30 piksel kaydırmayı deniyor:

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

overflow: hidden olan kutu, focus alan linki göstermek için 390 piksel kaydı ve JavaScript'in scrollLeft = 30'unu da kabul etti. hidden yalnız scrollbar'ı ve kullanıcının tekerlekle kaydırmasını kapatır; kutu hâlâ bir scroll container'dır. Focus, scrollIntoView(), sayfa içi arama ve anchor linkleri onu kaydırabilir. overflow: clip ise taşanı gerçekten keser: kutu scroll container değildir, scrollLeft hep 0 kalır. Giriş sorusunun cevabı: yalnız hidden olan kutu kayar. Bu, iki motorda da aynı.

Fark sticky element'lerde daha çok hissedilir: position: sticky en yakın scroll container'a yapışır, araya giren bir overflow: hidden ata sticky'yi bozar. Bunu ve overflow-x: hidden'ın öbür ekseni auto'ya çevirmesini sitedeki CSS serisinin (CSS3: Flexbox, Grid ve Modern CSS) 04. yazısında ölçmüştük. clip'in bir avantajı daha var: bir eksende clip, öbüründe visible yazılabilir; hidden bu kombinasyonda öbür ekseni auto'ya çevirir. Taşanı kesme çizgisini dışarı kaydıran overflow-clip-margin ise Baseline değil.

auto ile scroll arasındaki fark scrollbar'ın ne zaman çizildiği: auto yalnız içerik taştığında çizer, scroll içerik sığsa da çizer (klasik scrollbar'ı olan sistemlerde boş bir ray görünür). Bookstore'da kapak çerçevesi, hover'da büyüyen görseli kesmek için overflow: hidden kullanıyordu; çerçevenin kaydırılması hiçbir zaman istenmediği için doğru değer clip:

.book-card .cover {
    margin-block-end: 0.75rem;
    border-radius: 0.25rem;
    /* büyüyen görsel çerçevenin içinde kalır; clip: çerçeve scroll container değil */
    overflow: clip;
}

Şeride snap eklemeden önce telefon boyutunda (480×800) ölçtüm ve bir sürprizle karşılaştım:

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

İlk satır, flex: none olmadan kapakların 160 yerine 45.5 (WebKit'te 48) piksele indiğini ve şeridin hiç kaymadığını gösteriyor (scrollWidth ile clientWidth eşit). Sebep, serinin görseller yazısında eklediğimiz img reset'i: max-inline-size: 100% alan bir görselin minimum genişliği sıfır sayılır. Flex item'ın otomatik minimumu (min-width: auto, CSS3 serisinin 05. yazısı) içeriğinden geldiği için li'yi koruyacak bir şey kalmıyor ve varsayılan flex-shrink: 1 kapakları sıkıştırıyor. Kaydırılması gereken bir şeritte item'lar küçülmemeli: flex: none. Düzeltmeden sonra şerit 1104 piksel genişliğinde ve kayıyor.

Şimdi snap. scroll-snap-type container'a, scroll-snap-align item'lara yazılır:

.book-strip {
    display: flex;
    gap: 1rem;
    margin: 0;
    /* ilk ve son kapak soldurulmuş kenarların dışında başlar */
    padding: 0 2rem;
    list-style: none;
    overflow-x: auto;
    /* tam kapaklara oturt; snap çizgisi soldurulmuş kenardan sonra başlar */
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 2rem;
    /* sondaki yana kaydırma tarayıcı geçmişinde "geri"ye dönüşmez */
    overscroll-behavior-inline: contain;
    /* soldurulmuş kenarlar: satırın yana kaydığına dair ipucu */
    mask-image: linear-gradient(to right, transparent, #000000 2rem, #000000 calc(100% - 2rem), transparent);
}
.book-strip li {
    /* küçülmesin: yüzdelik max-inline-size'lı bir img, item'a minimum genişlik vermez */
    flex: none;
    scroll-snap-align: start;
}
.book-strip img { inline-size: 10rem; border-radius: 0.25rem; }

x mandatory, yatay kaydırma her bittiğinde bir snap noktasında durmayı zorunlu kılar; proximity ise yalnız bir noktaya yakınken oturtur. scroll-snap-align: start, kapağın başlangıcını snap çizgisine hizalar. Snap çizgisi varsayılan olarak container'ın kenarıdır; ama bu şeridin kenarları serinin clip-path ve mask yazısında mask-image ile soldurulmuştu. scroll-padding-inline: 2rem snap çizgisini 32 piksel içeri alır ve kapak soldurulmuş kısmın arkasında durmaz.

Ölçümde scrollTo(100), iki motorda da 176'da bitti: 160 piksel kapak artı 16 piksel boşluk, yani ikinci kapak tam snap çizgisinde. Klavye de aynı kurala uyuyor: altıncı kapağa Tab ile gelince şerit sonuna kadar kaydı (Chromium'da 687, WebKit'te 672; ikisi de en büyük değer). İki motorun clientWidth'i farklı, çünkü Chromium sayfada scrollbar için 15 piksel yer ayırıyor; sebebini scrollbar bölümünde göreceğiz.

İki ek property daha var. scroll-snap-stop: always bir item'a yazılırsa hızlı bir kaydırma o item'ın üzerinden atlayamaz; bir defada tek kart ilerleyen bir galeri için kullanılır. overscroll-behavior-inline: contain ise şeridin sonuna gelindiğinde yatay kaydırmanın sayfaya geçmesini engeller; spesifikasyona göre bu, trackpad ve dokunmatik ekranlarda yana kaydırmanın tarayıcı geçmişinde "geri" gitmesini de keser. Bu son davranışı gerçek bir trackpad ya da telefonla denemedim.

scroll-padding, scroll-margin ve scroll-behavior

Katalogdaki filtre çubuğu sticky ve sayfanın üstüne yapışıyor. Header'daki "Reading list" linkine tıklayınca tarayıcı #reading-list başlığını sayfanın en üstüne getirir, yani tam çubuğun altına. Önce sorunu lab'de 50 piksellik bir sticky header'la görelim:

/* 2) scroll-margin: hedef sticky header'dan mesafesini korur */
#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)

scrollIntoView() ile getirilen sade başlık 0. piksele, header'ın altına düştü. scroll-margin-block-start: 60px verilen başlık ise 60. pikselde, header'ın 10 piksel aşağısında. scroll-margin hedef element'e yazılır ve yalnız onu etkiler. Sayfanın bütün hedefleri için tek bir değer istiyorsan scroll-padding'i scroll container'a yazarsın; sayfa kaydırması için bu container html'dir. Bookstore'da değeri, sorunu yaratan filtre çubuğu dosyasına koyduk; :has() ile yalnız filtre çubuğu olan sayfalarda geçerli (selector tarafı CSS3 serisinin 02. yazısında):

/* anchor hedefleri sticky çubuğun altında değil, hemen aşağısında durur */
html:has(.filter-bar) { scroll-padding-block-start: 4rem; }

base.css'te de anchor geçişini yumuşatıyoruz. scroll-behavior: smooth, anchor linklerine ve behavior belirtmeyen scrollTo() çağrılarına uygulanır. Hareketi azaltmak isteyen kullanıcıya animasyon göstermemek için onu prefers-reduced-motion'ın no-preference koluna koyuyoruz (kuralın gerekçesi CSS3 serisinin 13. yazısında). Dosyadaki scrollbar-gutter satırına sepet bölümünde döneceğiz:

html {
    /* sayfa kaymasa da scrollbar'ın yeri korunur (modal kilitlediğinde kayma olmaz) */
    scrollbar-gutter: stable;
}
/* anchor'lara yumuşak geçiş, yalnız daha az hareket istemeyenler için */
@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" sayfanın son bölümü olduğu için en üste kadar çıkamıyordu; ölçümde main'in altına bir ekran boyu boşluk ekledim. Sonuç iki motorda da aynı: çubuğun alt kenarı 52. pikselde, başlık 64. pikselde (4rem). Normal ayarda geçiş animasyonlu, reducedMotion: 'reduce' ile açılan sayfada başlık tek adımda yerine geliyor. Çubuğun yüksekliği font ve satır yüksekliğiyle değişebileceği için 4rem'i ölçülen 52 pikselin biraz üstünde tuttuk.

overscroll-behavior ve Scroll Lock: Sepet Drawer'ı

Kaydırılan bir kutu sonuna gelince tarayıcı kaydırmayı bir üstteki scroll container'a, sonunda sayfaya devreder. Buna scroll chaining denir ve bir drawer'ın listesini kaydırırken arkadaki sayfanın da kaymasının sebebi budur. Lab'de sağ üstte sabit bir panel var; panel sona kaydırılıyor, sonra üzerinde tekerlek 300 piksel çevriliyor:

/* 4) scroll chaining: panel sona gelince ne olur? */
#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

Uzun içerikli panelde overscroll-behavior: contain iki motorda da işini yapıyor: sayfa yerinde kalıyor. Kısa içerikte (panel scroll container ama kaydırılacak bir şey yok) fark ortaya çıkıyor: Chromium yine sayfayı tutuyor, WebKit ise 300 piksel kaydırıyor. browser-compat-data'nın notu tam bunu söylüyor: contain, kaydırılabilir taşması olmayan scroll container'larda Safari'de etkisiz; Chrome bunu 144'te, Firefox 150'de düzeltti. web-features'ın overscroll-behavior'ı "Baseline değil" saymasının sebebi bu. Temel kullanım (Chrome 63, Firefox 59, Safari 16'dan beri) her yerde çalışıyor; Android'deki pull-to-refresh'i ve sayfaya geçen kaydırmayı kesmek için güvenle kullanılabilir.

Bookstore'un sepeti, katalogda header'daki butonla açılan modal bir <dialog> (element'in kendisi sitedeki HTML serisinin (HTML5 ve Semantic HTML) 06. yazısında). Yalnız ürün listesi kayıyor:

<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>
/* sepet sağ kenara yaslanmış modal bir dialog olarak açılır */
.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; }

/* yalnız liste kayar; header ve footer yerinde kalır */
.cart-items {
    flex: 1;
    min-block-size: 0;
    margin: 0;
    padding: 0 1.5rem;
    list-style: none;
    overflow-y: auto;
    /* listenin sonu, kaydırmayı sayfaya devretmez */
    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: drawer açıkken arkadaki sayfa kaymaz */
html:has(.cart-drawer[open]) { overflow: hidden; }

Modal dialog arkadaki sayfayı inert yapar ama kaydırılmasını engellemez: backdrop'un üzerinde tekerlek çevirmek sayfayı kaydırır. Bu yüzden iki önlem var. Liste için overscroll-behavior: contain; dialog açıkken sayfa için html'e overflow: hidden (scroll lock). Ölçüm, sayfa Windows'taki gibi klasik scrollbar'larla açıldığında üç durumu karşılaştırıyor:

--- 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

Yazdığımız hâliyle sayfa kaymıyor ve main 465 pikselde kalıyor. scrollbar-gutter'ı kaldırınca kilit scrollbar'ı yok ettiği anda main 465'ten 480'e genişliyor: arkadaki bütün içerik 15 piksel sağa kayıyor. Kilidi kaldırınca backdrop'taki tekerlek sayfayı 400 piksel kaydırıyor. Liste 624 piksellik alanda 784 piksel içerik taşıyor, yani contain'in gerçekten iş yaptığı bir scroll container. WebKit macOS'ta overlay scrollbar (içeriğin üstünde beliren, yer kaplamayan scrollbar) kullandığı için genişlik hep 480; kaymayı orada göremezsin.

iOS Safari için bir uyarı: html ya da body'deki overflow: hidden'ın iPhone'da sayfayı tam kilitlemediği yıllarca raporlandı (WebKit'in 153852 numaralı kaydı düzeltildi olarak kapatıldı ama benzer şikâyetler sonraki sürümlerde de geldi). Yaygın çözüm, açılışta body'yi position: fixed yapıp kaydırma konumunu saklamak ve kapanışta geri yüklemek. Bunu gerçek bir iOS cihazında denemedim; drawer'ı yayına almadan önce hedeflediğin iOS sürümünde dene.

touch-action

touch-action, bir element'in üzerinde başlayan parmak hareketlerinden hangilerini tarayıcının kendisinin işleyeceğini söyler; geri kalanı JavaScript'e kalır. Playwright WebKit'te dokunmatik hareket üretemediğim için bu ölçüm yalnız Chromium'da, DevTools protokolünün dokunmatik kaydırma komutuyla:

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

pan-y yazılan yatay şerit, yana parmak hareketiyle kaymıyor ama yukarı hareket sayfayı kaydırıyor. Bu, yana sürükleme hareketini kendin yazdığın bir slider ya da "kaydırarak sil" listesi için doğru değer: dikey sayfa kaydırması bozulmadan yatay hareket senin kodunun. pan-x tersini yapıyor. manipulation kaydırmaya ve iki parmakla yakınlaştırmaya izin verir, yalnız çift dokunuşla yakınlaştırmayı kapatır; tarayıcı ikinci dokunuşu beklemediği için butonlarda tıklama gecikmesi kalkar. Bir sonraki yazıdaki adet butonlarında bunu kullanacağız. touch-action: none her şeyi kapatır ve yalnız bir çizim alanı ya da harita gibi hareketi tamamen kendin yöneteceğin yerlerde doğru.

Scrollbar Stilleri: scrollbar-gutter, scrollbar-width ve scrollbar-color

Scrollbar'lar sisteme göre iki türlü: Windows'ta (ve çoğu Linux masaüstünde) klasik scrollbar yer kaplar, macOS ve mobilde overlay scrollbar içeriğin üstünde belirir. Playwright'ın headless Chromium'u scrollbar'ları varsayılan olarak gizliyor; bu yazıdaki ölçümler için browser.mjs'e bunu kapatan bir scrollbars seçeneği ekledim, böylece Chromium Windows'taki gibi klasik scrollbar çiziyor:

/* 3) scrollbar'lar */
.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  -

Chromium'da taşan kutunun scrollbar'ı 15 piksel. Taşmayan kutuda scrollbar yok ve içerik 15 piksel daha geniş; kutunun içeriği uzayınca bütün satırlar yeniden kırılır. scrollbar-gutter: stable içerik taşmasa da bu 15 pikseli ayırır, bu yüzden Bookstore'da html'e yazdık: drawer kilidi scrollbar'ı kaldırdığında sayfa kaymadı. scrollbar-width: thin scrollbar'ı 11 piksele indirdi, scrollbar-color ise sırasıyla thumb'ı (sürüklenen parça) kırmızı, ray'ı sarı boyadı. Bu iki property yalnız iki renk ve üç genişlik değeri (auto, thin, none) sunar; bilinçli olarak sınırlı.

Eski kodlarda gördüğün ::-webkit-scrollbar pseudo-element'leri standart değil ama Chromium ve WebKit'te hâlâ çalışıyor: #legacy 8 piksel ve yeşil thumb'lı. #both'ta ikisi bir arada ve standart property kazanıyor: Chromium'da 15 piksel, kırmızı-sarı. Chromium, scrollbar-color verilen element'te ::-webkit-scrollbar kurallarını uygulamadı; Chrome 121'de standart property'ler gelince birçok sitenin eski scrollbar stilinin birden değişmesinin sebebi bu. Eski stili korumak istiyorsan onu @supports not (scrollbar-color: auto) içine al; Safari 26.2'den beri scrollbar-color'ı desteklediği için bugün bu blok neredeyse hiçbir yerde çalışmaz. WebKit'in macOS'taki overlay scrollbar'ı genişlik ölçümlerinde hep 0 çıktı.

Üretimde Dikkat

Scroll davranışını Windows'ta, klasik scrollbar'larla kontrol et. macOS'ta geliştirip Windows'ta açılan bir sayfada en sık görülen iki sorun, içerik değişince 15 piksel oynayan layout ve 100vw genişliğindeki bir bölümün yatay scrollbar çıkarması (100vw scrollbar'ın genişliğini de içerir). scrollbar-gutter: stable birincisini, 100% ya da inline-size: auto ikincisini çözer.

Snap'li bir şeride sonradan item eklenirse ya da görseller yüklenip boyut değişirse spesifikasyon tarayıcının konumu yeniden snap noktasına oturtmasını ister; bu yüzden şeritteki görsellere width ve height vermek (HTML5 serisinin 08. yazısı) snap için de önemli. mandatory'yi, item'ı ekrandan büyük olabilecek bir container'da kullanma: item'ın ortasındaki içeriğe hiç ulaşılamayabilir. Böyle yerlerde proximity daha güvenli.

Scroll lock'u açık bırakan bir hata sayfayı kaydırılamaz hâle getirir; örneğin dialog close çağrılmadan DOM'dan (sayfanın element ağacı) silinirse. Kilidi JavaScript'te bir class ile değil, burada olduğu gibi :has(.cart-drawer[open]) ile dialog'un kendi durumuna bağlamak bu hatayı önler. Kaydırmayla tetiklenen animasyonlar bu yazının değil, CSS3 serisinin 14. yazısının konusu.

Sık Düşülen Tuzaklar

Birincisi, taşanı kesmek için overflow: hidden kullanıp içeriğin focus ve anchor'larla kaydırılabileceğini unutmak. Kaydırma hiç istenmiyorsa clip yaz.

İkincisi, kaydırılan bir flex şeritte item'ların küçülmesi. Yüzdelik max-inline-size alan görseller item'a minimum genişlik vermez; item'lara flex: none ver.

Üçüncüsü, body'ye overflow: hidden yazıp scroll lock'un her yerde çalıştığını varsaymak. iOS Safari'de sızabilir ve klasik scrollbar'lı sistemlerde scrollbar-gutter olmadan sayfayı 15 piksel kaydırır.

Dördüncüsü, scrollbar'ı tamamen gizlemek (scrollbar-width: none). Bir alanın kaydığını gösteren başka bir ipucu yoksa (yarım görünen kapak, soldurulmuş kenar) kullanıcı devamını fark etmez.

Beşincisi, overscroll-behavior: contain'in kısa içerikli bir panelde de sayfayı tutacağını sanmak. Safari'de taşması olmayan scroll container'da etkisiz; kilit gerekiyorsa ayrıca yaz.

Baseline Durumu

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
ÖzellikDurumNot
overflow: clipWidely available (2025-03)overflow-clip-margin Baseline değil
Scroll snap, scroll-padding, scroll-marginWidely availablescroll-snap-stop 2022-07'den beri
scroll-behaviorWidely available (2024-09)Reduced motion'a bağla
overscroll-behaviorBaseline değilTemel kullanım her yerde; Safari'de taşmasız container'da etkisiz
touch-actionWidely availablepan-up/pan-left gibi yönlü değerler yalnız Chromium'da
scrollbar-gutter, scrollbar-widthNewly available (2024-12)Overlay scrollbar'da etkisiz
scrollbar-colorNewly available (2025-12)Safari 26.2; ::-webkit-scrollbar'ı geçersiz kılar

Küçük Bir Deneme

★☆☆ Şeritte mandatory'yi proximity'ye çevir ve 11-pages.mjs'te scrollTo(100)'ün nerede bittiğine bak; sonra 150 ile dene.

★★☆ Katalogdaki scroll-padding-block-start'ı kaldırıp aynı sonucu yalnız okuma listesi başlığına scroll-margin-block-start vererek elde et; iki yolun hangi durumda farklı sonuç vereceğini (sayfada ikinci bir hedef olduğunda) yaz.

★★★ Sepet listesini iki kitaba indir ve lab'deki panel ölçümünü drawer'a uygula: kilit kapalıyken liste üzerinde tekerlek çevirince WebKit'te sayfa kayıyor mu? Sonra contain'in yerine geçebilecek bir önlem öner ve iki motorda ölç.

Bir sonraki yazıda form kontrollerine geçiyoruz: accent-color, appearance: none ile özel checkbox, field-sizing ile büyüyen textarea, cursor, pointer-events ve user-select; Bookstore'un checkout form'u adet butonları ve sipariş notuyla tamamlanacak.