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

Border, border-radius, outline ve box-shadow: Kenarlar, Gölgeler ve Focus Halkası

Mert Özen 15 Kas 2026 13 dk 6 görüntülenme
Border, border-radius, outline ve box-shadow: Kenarlar, Gölgeler ve Focus Halkası

Seçili ya da hover durumunu kenarlık kalınlığıyla değil renk, inset box-shadow veya outline ile gösterirsin; focus halkasını :focus-visible ve outline ile çizersin, çünkü box-shadow forced colors modunda kaybolur. Bookstore'un checkout ve kataloğunda kenarlığın içeriği kaydırmasını, border shorthand'inin border-image'ı silmesini, 50% ile 9999px pill farkını, border-radius'u takip etmeyen border-image'ı ve forced colors'da kaybolan gölgeyi ölçüyoruz.

Sürüm notu: Bu yazı CSS Backgrounds and Borders Level 3/4 ve CSS Basic User Interface Level 4 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 checkout sayfasına kargo seçimi ekliyoruz. Her seçenek bir kart; seçili kartı belli etmenin üç yolunu denedik: kenarlığı kalınlaştırmak, içeriden bir gölge vermek ve bir outline çizmek.

.option { inline-size: 240px; padding: 12px; border: 1px solid #d1d5db; border-radius: 12px; }
.option.thick-border { border-width: 3px; }
.option.inset { box-shadow: inset 0 0 0 3px #1d4ed8; }
.option.outlined { outline: 3px solid #1d4ed8; }

Dört kartın içinde aynı metin var. Hangilerinde metin yerinden oynar, hangilerinde kartın boyutu değişir? Cevabını not et; ilk ölçüm bununla ilgili.

border Shorthand'i, Kenar Bazlı Longhand'ler ve border-style

border, dört kenarın genişliğini, stilini ve rengini tek seferde verir; border-top gibi kenar shorthand'leri ve border-top-width gibi longhand'ler de var. Mantıksal karşılıkları (border-inline-start, border-block-end) yazı yönüne göre çalışır; ayrıntısı sitedeki CSS serisinin (CSS3: Flexbox, Grid ve Modern CSS) 03. yazısında. Lab'deki ilk ölçüm:

node scripts/05-edges.mjs
plain     box 266x50  text at x=13, y=16
thick     box 270x54  text at x=15, y=18
inset     box 266x50  text at x=13, y=16
outlined  box 266x50  text at x=13, y=16

Giriş sorusunun cevabı: yalnız kalın kenarlıklı kartta hem kutu (266×50'den 270×54'e) hem metin (x=13'ten 15'e) değişiyor. Kenarlık box model'in bir katmanı ve 1px'ten 3px'e çıkınca her kenarda 2px yer kaplıyor. inset gölge ve outline ise kutunun boyutuna hiç girmiyor: ikisinde de ölçüler düz kartla aynı. Kenarlık kalınlığını bir durum değişikliğinde (seçim, hover, hata) değiştirmek bu yüzden komşu içeriği iter; listede bir kart seçildiğinde altındaki her şey 4px aşağı kayar.

border-style'ın on değeri var: none, hidden, solid, dashed, dotted, double, groove, ridge, inset ve outset. Başlangıç değeri none, bu yüzden border-width: 2px tek başına hiçbir şey çizmez; stil olmadan genişlik 0'a hesaplanır. border-color yazılmazsa currentColor kullanılır.

border shorthand'i de sıfırlama kuralına uyar, üstelik bir sürprizle: kendi yazamadığı border-image'ı da başlangıç değerine çeker.

.frame { border: 4px solid; border-image: linear-gradient(90deg, #b91c1c, #1d4ed8) 1; }
.frame.reset { border: 4px solid #1f2937; }
frame        border-image-source: linear-gradient(90deg, rgb(185
frame-reset  border-image-source: none

İkinci kural yalnız kenarlığın rengini değiştirmek istiyordu, ama gradient kenarlık tamamen kayboldu. Değişen tek şey renkse border-color yaz.

border-radius: Yüzde, Eliptik Köşe ve Pill

border-radius her köşe için bir yarıçap alır; dört değer sol üstten başlayıp saat yönünde gider. Her köşe aslında bir elips çeyreği: / işaretinden önceki değerler yatay, sonrakiler dikey yarıçaplardır (border-radius: 40px / 20px). Yüzdeler de bu yüzden iki eksende ayrı hesaplanır: yatay yarıçap genişliğin, dikey yarıçap yüksekliğin yüzdesi. Pill (hap) biçimli bir buton için iki yazım görürsün; 200×40'lık bir kutuda farkı ölçtüm:

.pill { inline-size: 200px; block-size: 40px; background: #1d4ed8; }
.pill.percent { border-radius: 50%; }
.pill.length { border-radius: 9999px; }
percent  radius 50%     pixel (20, 2): rgb(255, 255, 255)
length   radius 9999px  pixel (20, 2): rgb(29, 78, 216)

Sol kenardan 20, üstten 2 piksel içerideki nokta 50%'de boyanmamış (beyaz), 9999px'te boyanmış (mavi). 50% yatayda 100px, dikeyde 20px yarıçaplı bir elips üretir ve kutu iki yanı sivri bir yumurtaya döner. 9999px ise iki köşenin yarıçap toplamı kenarı aşınca orantılı olarak küçültülür ve 20px'lik yarım dairelere iner; gerçek hap biçimi budur. Kutunun boyutu değişse de doğru çalıştığı için pill butonlarda büyük bir uzunluk kullan.

Köşe biçiminin kendisi de değişmek üzere: corner-shape property'si bevel, notch, squircle gibi değerlerle yuvarlak yerine kesik ya da yumuşak kare köşeler çiziyor. Chrome 139'dan beri var; browser-compat-data'ya göre Safari 27.3 ile geliyor, Firefox'ta yalnız geliştirme sürümünde. Playwright'ın WebKit 27.2 build'i değeri şimdiden kabul ediyor, ama bu yayındaki Safari değil. Baseline değil; desteklemeyen tarayıcı normal yuvarlak köşeyi çizdiği için progressive enhancement (eski tarayıcıda da çalışan bir temel üzerine destekleyen tarayıcıya ek iyileştirme koymak) olarak denenebilir.

Gradient Kenarlık: border-image mı, background-clip mi?

Katalogda "haftanın kitabı" kartına kırmızıdan maviye giden bir kenarlık istiyoruz. İlk akla gelen border-image, ama köşeleri yuvarlak bir kartta bir sorunu var:

.grad { inline-size: 160px; block-size: 60px; border-radius: 16px; }
.grad.image { border: 4px solid; border-image: linear-gradient(135deg, #b91c1c, #1d4ed8) 1; }
.grad.clip {
    border: 4px solid transparent;
    background:
        linear-gradient(#ffffff, #ffffff) padding-box,
        linear-gradient(135deg, #b91c1c, #1d4ed8) border-box;
}
grad-image corner (1, 1): rgb(184, 28, 28)  top border (80, 1): rgb(132, 45, 92)
grad-clip  corner (1, 1): rgb(255, 255, 255)  top border (80, 1): rgb(131, 45, 93)

Kenarlığın ortasında iki yöntem de aynı rengi veriyor. Köşede ise border-image'lı kutu kırmızı: border-image, border-radius'u yok sayar ve köşeleri kare çizer. İkinci yöntem iki background katmanı kullanıyor: üstte padding-box'a kadar boyanan düz beyaz bir katman, altta border-box'a kadar boyanan gradient. Kenarlık şeffaf olduğu için arkasındaki gradient görünüyor ve iki katman da border-radius'a göre kırpılıyor; köşe noktası beyaz. Önceki yazıdaki background-clip ve çoklu katman bilgisinin doğrudan uygulaması bu. border-image'ın yeri ise köşesi düz kutularda dokulu ya da 9 parçaya bölünmüş görsel kenarlıklar.

outline, outline-offset ve Focus Halkası

Outline kenarlığa benzer ama box model'in parçası değildir: yer kaplamaz, komşuları itmez ve kutunun dışına çizilir. outline-offset onu kutudan uzaklaştırır, negatif değer içeri çeker. Eskiden outline her zaman dikdörtgen çizilirdi; Safari 16.4 ile Mart 2023'ten beri üç motor da border-radius'u takip ediyor. Köşesi 12px yuvarlatılmış bir kutunun outline'ında köşe ve kenar noktalarına baktım:

.ring { inline-size: 120px; block-size: 60px; border-radius: 12px; background: #e5e7eb;
        outline: 3px solid #b91c1c; outline-offset: 4px; }
outline  corner (-6, -6): rgb(255, 255, 255)  edge (60, -6): rgb(185, 28, 28)

Kenar noktası kırmızı, köşe noktası beyaz: outline köşede kutunun yuvarlaklığını takip ediyor. Outline'ın klavye kullanıcısı için en önemli rolü focus halkası. :focus-visible pseudo-class'ı tarayıcının "bu focus'u göstermek gerekir" dediği durumları seçer: klavye ile gelinen her focus ve metin girilen alanlara gelen her focus. Fare ile tıklanan bir butonda ise genellikle eşleşmez. Lab'de ölçtüm:

.field:focus-visible { outline: 3px solid #1d4ed8; outline-offset: 2px; }
.no-ring:focus { outline: none; }
.shadow-ring:focus-visible { outline: none; box-shadow: 0 0 0 3px #1d4ed8; }
.transparent-ring:focus-visible { outline: 3px solid transparent; box-shadow: 0 0 0 3px #1d4ed8; }
--- chromium
input  after Tab:   :focus=true :focus-visible=true outline=solid
button after click: :focus=true :focus-visible=false outline=none
--- webkit
input  after Tab:   :focus=true :focus-visible=true outline=solid
button after click: :focus=false :focus-visible=false outline=none

Tab ile gelinen input'ta :focus-visible eşleşiyor ve outline çiziliyor. Tıklanan butonda Chromium focus'u veriyor ama :focus-visible eşleşmiyor; fare kullanıcısı tıkladığı butonda halka görmüyor. WebKit macOS davranışını izliyor ve tıklanan butona focus'u hiç vermiyor. Selector'ün kendisi CSS3 serisinin 02. yazısında; focus yönetiminin kuralları ise Accessibility (a11y) serisinin konusu.

Halkayı outline yerine box-shadow ile çizen kodlar sık görülür, çünkü gölge border-radius'u her zaman takip etti, outline ise yakın zamana kadar etmiyordu. Bunun bir bedeli var. Windows'un yüksek contrast teması gibi forced colors modunda tarayıcı sayfanın renklerini kullanıcının paletine çevirir ve box-shadow'u tamamen kaldırır:

--- chromium, forced colors
shadow-ring       box-shadow: none  outline: none rgba(5, 0, 73, 0.8)
transparent-ring  box-shadow: none  outline: solid rgba(5, 0, 73, 0.8)

Yalnız gölgeyle çizilen halka (shadow-ring) forced colors'da tamamen kayboluyor: gölge none, outline none. Aynı gölgenin yanına outline: 3px solid transparent yazdığımız alanda ise outline duruyor ve rengi artık şeffaf değil; forced colors modu outline'ı kendi paletinden bir renkle çiziyor. Normal modda şeffaf outline görünmüyor, gölge görünüyor; forced colors'da gölge gidiyor, outline görünüyor. Bookstore'un base stilinde ise doğrudan outline kullanıyoruz:

:focus-visible {
    /* transparent outline'lar forced colors modunda görünür hâle gelir */
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
}

box-shadow: Offset, Blur, Spread, Inset ve Elevation Ölçeği

box-shadow sırasıyla yatay offset, dikey offset, blur, spread ve renkten oluşur; başına inset yazılırsa kutunun içine düşer. Spread gölgeyi her yönde büyütür (negatif değer küçültür); blur'u 0 ve spread'i pozitif bir gölge, kutunun etrafında katı bir halka çizer. Virgülle birden fazla gölge yazılabilir, ilk yazılan en üstte çizilir:

card box-shadow: rgba(0, 0, 0, 0.08) 0px 1px 2px 0px, rgba(0, 0, 0, 0.12) 0px 4px 12px 0px

Computed değerde renk başa alınmış ve spread 0 olarak eklenmiş. Gerçekçi bir gölge için iki katman iyi çalışıyor: kutunun hemen altında sıkı ve hafif koyu bir temas gölgesi, daha uzağa yayılan yumuşak bir ortam gölgesi. Bookstore'da bu çiftleri üç seviyelik bir elevation ölçeğine çevirip custom property'lerde tutuyoruz; kart normalde sm, hover'da lg alıyor:

:root {
    --color-focus: #1d4ed8;
    /* elevation ölçeği: sıkı bir temas gölgesi ve yumuşak bir ortam gölgesi */
    --shadow-sm: 0 1px 2px rgb(15 23 42 / 0.08), 0 1px 3px rgb(15 23 42 / 0.06);
    --shadow-md: 0 2px 4px rgb(15 23 42 / 0.08), 0 4px 12px rgb(15 23 42 / 0.10);
    --shadow-lg: 0 4px 8px rgb(15 23 42 / 0.08), 0 12px 32px rgb(15 23 42 / 0.14);
}
.book-card {
    padding: 1rem;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 0.5rem;
    box-shadow: var(--shadow-sm);
}
.book-card:hover { box-shadow: var(--shadow-lg); }

/* border-radius'u takip eden gradient kenarlık */
.book-card.featured {
    border: 2px solid transparent;
    background:
        linear-gradient(var(--color-surface), var(--color-surface)) padding-box,
        linear-gradient(135deg, var(--color-accent), var(--color-link)) border-box;
}
node scripts/05-pages.mjs
--- chromium
express selected: box-shadow rgb(29, 78, 216) 0px 0px 0px 1px inset
price moved: false
#email after Tab: :focus-visible=true outline 3px solid rgb(29, 78, 216), offset 2px
featured card corner pixel: rgb(249, 250, 251) (page background is rgb(249, 250, 251))
card shadow:       rgba(15, 23, 42, 0.08) 0px 1px 2px 0px, rgba(15, 23, 42, 0.06) 0px 1px 3px 0px
card shadow hover: rgba(15, 23, 42, 0.08) 0px 4px 8px 0px, rgba(15, 23, 42, 0.14) 0px 12px 32px 0px
box moved on hover: false
--- webkit
express selected: box-shadow rgb(29, 78, 216) 0px 0px 0px 1px inset
price moved: false
#email after Tab: :focus-visible=true outline 3px solid rgb(29, 78, 216), offset 2px
featured card corner pixel: rgb(249, 250, 251) (page background is rgb(249, 250, 251))
card shadow:       rgba(15, 23, 42, 0.08) 0px 1px 2px 0px, rgba(15, 23, 42, 0.06) 0px 1px 3px 0px
card shadow hover: rgba(15, 23, 42, 0.08) 0px 4px 8px 0px, rgba(15, 23, 42, 0.14) 0px 12px 32px 0px
box moved on hover: false
--- chromium, forced colors
selected   option: box-shadow none, border-color rgb(0, 0, 0)
unselected option: box-shadow none, border-color rgb(0, 0, 0)
#email focus outline: solid 3px

Hover'da gölge değişiyor ama kartın kutusu kımıldamıyor; gölge de outline gibi layout'a girmiyor. Öne çıkan kartın sol üst köşe pikseli sayfa arka planıyla aynı, yani gradient kenarlık köşede yuvarlanıyor. Gölge renginde saf siyah yerine arayüzün koyu lacivertini (rgb(15 23 42)) kullanmak gölgenin zemine daha doğal oturmasını sağlıyor. Gölgeyi bir şeklin kenarına göre (örneğin arka planı şeffaf bir kapak görselinin kendi biçimine göre) düşürmek gerekiyorsa box-shadow değil filter: drop-shadow() kullanılır; o, bu serinin filter yazısının konusu.

Kargo Seçimi: Kenarlık Yerine inset Gölge

Checkout sayfasında giriş sorusundaki dersi uyguluyoruz. Her kargo seçeneği bir label; içindeki radio seçilince kart :has(:checked) ile seçili görünüme geçiyor:

<fieldset class="shipping-options">
    <legend>Shipping</legend>
    <label class="shipping-option">
        <input type="radio" name="shipping" value="standard" checked>
        Standard (3-5 days) <span class="cost">₺49.90</span>
    </label>
    <label class="shipping-option">
        <input type="radio" name="shipping" value="express">
        Express (next day) <span class="cost">₺89.90</span>
    </label>
</fieldset>
.shipping-option {
    display: flex;
    gap: 0.75rem;
    align-items: center;
    padding: 0.75rem 1rem;
    border: 1px solid var(--color-border);
    border-radius: 0.75rem;
    background: var(--color-surface);
}
/* seçili: kenarlık rengi + 1px inset gölge = 2px halka, layout kaymaz */
.shipping-option:has(:checked) {
    border-color: var(--color-link);
    box-shadow: inset 0 0 0 1px var(--color-link);
}

Seçili kartta kenarlığın genişliği değişmiyor; yalnız rengi değişiyor ve içeriden 1px'lik bir gölge ekleniyor. Görünen etki 2px'lik bir halka, ama kutunun boyutu aynı kalıyor. Yukarıdaki ölçümde Express seçildiğinde fiyat metni hiç kaymadı ve e-posta alanına Tab ile gelindiğinde 3px'lik outline 2px offset ile çizildi.

Forced colors satırları burada da bir şey söylüyor: seçili kartın gölgesi none'a dönüyor ve iki kartın kenarlık rengi de aynı sistem rengine (burada siyah) çevriliyor. Yani forced colors'da seçili kartı diğerlerinden ayıran tek şey radio'nun kendisi. Native radio'yu gizlemediğimiz için bu yeterli. Radio'yu appearance: none ile tamamen özelleştirseydik seçili durumu gösterecek görünür bir işaret (örneğin bir ikon ya da forced colors'da çalışan bir outline) gerekirdi; form kontrolleri bu serinin ilerideki form yazısında.

Üretimde Dikkat

box-shadow'u doğrudan animasyonla değiştirmek, her karede gölgenin yeniden çizilmesi demek; büyük blur değerlerinde bu pahalı. Hover'da yumuşak bir yükselme istiyorsan büyük gölgeyi bir ::after pseudo-element'ine koyup yalnız onun opacity'sini canlandırmak compositor'da çalışır. Yöntemin ölçümü CSS3 serisinin 13. yazısında. Biz burada gölgeyi animasyonsuz değiştiriyoruz.

Kenarlık, outline ve gölge kararlarını üç modda kontrol et: normal, dark mode ve forced colors. Playwright'ta forcedColors: 'active' ve colorScheme: 'dark' seçenekleriyle bu yazıdaki gibi computed değerleri okumak, gölgeye dayanan bir göstergenin kaybolduğunu erkenden yakalar. Dark mode'da gölgeler zor görünür; koyu zeminde yükseltiyi daha açık bir yüzey rengiyle vermek gerekir. Tema token'larının kendisi CSS3 serisinin 10. yazısında.

Sık Düşülen Tuzaklar

Birincisi, outline: none yazıp yerine bir şey koymamak. Klavye kullanıcısı nerede olduğunu göremez. Varsayılan halkayı beğenmiyorsan :focus-visible ile kendi outline'ını yaz; tamamen kaldırma.

İkincisi, hover ya da seçili durumda kenarlık kalınlığını değiştirmek. Ölçtüğümüz gibi kutu büyür ve içerik kayar. Kalınlığı sabit tut, rengini değiştir; ek kalınlık gerekiyorsa inset gölge ya da outline kullan.

Üçüncüsü, focus halkasını yalnız box-shadow ile çizmek. Forced colors'da gölge kalkar ve halka kaybolur; yanına şeffaf bir outline ekle ya da doğrudan outline kullan.

Dördüncüsü, köşesi yuvarlak bir kartta gradient kenarlık için border-image kullanmak. Köşeler kare kalır; iki katmanlı background-clip yöntemini kullan.

Baseline Durumu

node scripts/baseline.mjs borders border-radius border-image outline box-shadow focus-visible has corner-shape forced-colors
borders                    widely 2018-01-29  Chrome 1, Firefox 1, Safari 1, iOS 3
border-radius              widely 2018-01-29  Chrome 4, Firefox 4, Safari 5, iOS 4.2
border-image               widely 2019-08-01  Chrome 56, Firefox 50, Safari 9.1, iOS 9.3
outline                    widely 2025-09-27  Chrome 94, Firefox 88, Safari 16.4, iOS 16.4
box-shadow                 widely 2018-01-29  Chrome 10, Firefox 4, Safari 5.1, iOS 5
focus-visible              widely 2024-09-14  Chrome 86, Firefox 85, Safari 15.4, iOS 15.4
has                        widely 2026-06-19  Chrome 105, Firefox 121, Safari 15.4, iOS 15.4
corner-shape               not Baseline       Chrome 139, Firefox -, Safari -, iOS -
forced-colors              widely 2025-03-12  Chrome 89, Firefox 89, Safari 16, iOS 16
ÖzellikDurumNot
border, border-radius, box-shadowWidely availableShorthand border-image'ı da sıfırlar
border-imageWidely availableborder-radius'u yok sayar
outline (yuvarlak köşeyi takip etmesi)Widely available2023-03'te newly, 2025-09'da widely
:focus-visibleWidely availableTıklanan butonda genellikle eşleşmez
:has()Widely available2026-06'dan beri
forced-colors media featureWidely availableGölgeler bu modda kalkar
corner-shapeBaseline değilChrome 139; Safari 27.3 ile geliyor

Küçük Bir Deneme

★☆☆ Lab'deki .option.thick-border'ı, kalınlığı değiştirmeden aynı görünümü veren bir kurala çevir ve 05-edges.mjs ile kutunun 266×50'de kaldığını doğrula.

★★☆ Checkout'taki "Pay now" butonuna, pill biçimini takip eden ve butondan 3px uzakta duran bir :focus-visible halkası ver; forced colors modunda bu outline'ın kaldığını 05-pages.mjs'e eklediğin bir satırla ölç.

★★★ Kart hover gölgesini ::after pseudo-element'i ve opacity ile yap; normal ve hover durumlarında kartın kendi box-shadow'unun değişmediğini, yalnız pseudo-element'in opacity'sinin değiştiğini computed değerlerle göster.

Bir sonraki yazıda görsel efektlere geçiyoruz: translate, rotate ve scale function'ları ile bağımsız transform property'leri, sıranın sonucu nasıl değiştirdiği, transform-origin ve kitap detayında ön ve arka kapağı çeviren 3D bir kart.