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

Metin Stilleri: text-decoration Ailesi, text-underline-offset, text-shadow, letter-spacing, text-indent ve vertical-align

Mert Özen 15 Kas 2026 13 dk 7 görüntülenme
Metin Stilleri: text-decoration Ailesi, text-underline-offset, text-shadow, letter-spacing, text-indent ve vertical-align

Okunur bir link alt çizgisi longhand'lerle yazılır (text-decoration-thickness, text-underline-offset em ile), letter-spacing em ya da yüzdeyle verilir, inline ikon ise vertical-align ile rakamlara hizalanır. Bookstore kataloğunda text-decoration shorthand'inin kalınlığı sıfırlamasını, em'in px olarak kalıtılmasını, lang="tr" ile text-transform'u, img altındaki boşluğu ve initial-letter desteğini Chromium ve WebKit'te ölçüyoruz.

Sürüm notu: Bu yazı CSS Text Module Level 3/4 ve CSS Text Decoration 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

Önceki yazıda sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için font'u seçtik. Şimdi metnin kendisini biçimlendiriyoruz. Katalogdaki "New" etiketinin harf aralığını em ile verdik, kitap başlıklarındaki link'lere de kalın bir alt çizgi yazdık:

.badge, .badge-pct, .badge-none { font-size: 12px; text-transform: uppercase; }
.badge { letter-spacing: 0.1em; }
.badge-pct { letter-spacing: 10%; }
.big { font-size: 24px; }

.link { text-decoration-thickness: 2px; text-underline-offset: 0.2em; }
.link.reset { text-decoration: underline; }
<p class="badge">New <span class="big">New</span></p>
<p class="badge-pct">New <span class="big">New</span></p>
<p class="badge-none"><span class="big">New</span></p>

<p><a class="link" href="#">Dune</a> <a class="link reset" href="#">Emma</a></p>

İlk iki paragrafta 12px'lik etiketin içinde 24px'lik bir span var. Span'in letter-spacing'i ne olur: 1.2px mi, 2.4px mi? 10% yazdığımızda sonuç değişir mi? Son satırda .link.reset önce text-decoration-thickness: 2px alıyor, sonra text-decoration: underline shorthand'i. Emma'nın alt çizgisi kaç piksel kalınlıkta çizilir?

text-decoration bir shorthand ve dört longhand'i var: text-decoration-line (underline, overline, line-through), text-decoration-style (solid, double, dotted, dashed, wavy), text-decoration-color ve text-decoration-thickness. Shorthand'e dahil olmayan iki property daha var: çizginin metinden uzaklığını veren text-underline-offset ve çizginin harflerin aşağı uzanan kısımlarında (g, p, y gibi) kesilip kesilmeyeceğini belirleyen text-decoration-skip-ink. Lab'i iki motorda ölçtüm:

node scripts/02-text.mjs
--- chromium
.link            thickness 2px  offset 3.2px
.link.reset      thickness auto  offset 3.2px
hover: thickness 3px, box 95.5x19 → 95.5x19
--- webkit
.link            thickness 2px  offset 3.2px
.link.reset      thickness auto  offset 3.2px
hover: thickness 3px, box 95.5x18.1796875 → 95.5x18.1796875

Giriş sorusunun cevabı: Emma'nın kalınlığı auto, yani 2px kayboldu. Shorthand, yazmadığın longhand'leri başlangıç değerine çeker ve text-decoration-thickness artık shorthand'in parçası. text-underline-offset ise shorthand'e dahil olmadığı için 3.2px'te kaldı. Bir ayrıntı: Safari 26.2'den önce Safari'nin shorthand'i kalınlığı içermiyordu; bu yüzden aynı kod eski Safari'de 2px çizgiyi korur, diğer motorlarda sıfırlar. Tutarlı sonuç için link stillerinde longhand yaz.

Bookstore'un base stilinde link'ler şöyle:

a {
    color: var(--color-link);
    text-decoration-line: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.2em;
    text-decoration-skip-ink: auto;
}
a:hover {
    /* renk değil kalınlık: kutu yerinden oynamaz */
    text-decoration-thickness: 2px;
}

text-underline-offset: 0.2em çizgiyi harflerden biraz uzaklaştırır ve Türkçe'deki ç ve ş gibi altı işaretli harflerin çizgiye değmesini azaltır. Değeri em ile vermek, font büyüdüğünde boşluğun da büyümesini sağlar; ölçümde 16px font için 3.2px'e hesaplandı. text-decoration-skip-ink: auto zaten varsayılan, ama base stilde açıkça yazmak niyeti belli ediyor.

Hover'da renk değiştirmek yerine çizgiyi kalınlaştırıyoruz. Ölçümde hover sonrası kalınlık 3px'e çıktı, ama link'in kutusu iki motorda da aynı kaldı: çizgi metnin box'ına eklenmiyor, sadece üstüne çiziliyor. Bu yüzden kalınlık değişimi layout'u kaydırmaz. Katalog kartında başlıklar link, ama metinle aynı renkte görünmesini istiyoruz. Alt çizgiyi kaldırmak yerine rengini kenarlık rengine çekip hover'da currentColor'a getiriyoruz:

.book-card h2 a {
    color: inherit;
    text-decoration-color: var(--color-border);
}
.book-card h2 a:hover { text-decoration-color: currentColor; }
node scripts/02-pages.mjs
--- chromium
badge: font-size 12px, letter-spacing 0.96px
price icon center -6.0px, digits center -5.8px (from baseline)
initial-letter: true, -webkit-initial-letter: false
card link hover: 2px rgb(31, 41, 55), box moved: false
--- webkit
badge: font-size 12px, letter-spacing 0.96px
price icon center -6.0px, digits center -5.8px (from baseline)
initial-letter: false, -webkit-initial-letter: true
card link hover: 2px rgb(31, 41, 55), box moved: false

Son satırlarda kart link'inin hover'da 2px kalınlık ve metin rengi aldığı, kutunun ise kımıldamadığı görülüyor. Link'i metinden ayıran şeyin yalnız renk olmaması gerekir; alt çizgi, renk farkını göremeyen kullanıcılar için de çalışan bir ipucu. Kuralın ayrıntısı ve contrast eşikleri Accessibility (a11y) serisinin konusu.

letter-spacing ve word-spacing: em, px ya da Yüzde

Büyük harfli küçük etiketlerde harfler birbirine yapışık görünür, bu yüzden biraz letter-spacing eklenir. Giriş sorusunun ölçümü:

.badge            font-size 12px  letter-spacing 1.2px
.badge-pct        font-size 12px  letter-spacing 10%
.badge .big       font-size 24px  letter-spacing 1.2px   width 61.1px
.badge-pct .big   font-size 24px  letter-spacing 10%     width 64.7px
.badge-none .big  font-size 24px  letter-spacing normal  width 57.5px

0.1em, etiketin kendi 12px font'una göre 1.2px'e hesaplanıyor ve çocuk element'e bu px değeri kalıtılıyor. 24px'lik span de 1.2px alıyor: genişlik harf aralığı olmayan hâlden (57.5px) yalnız 3.6px fazla. Kalıtılan şey em değil, hesaplanmış uzunluk. 10% ise yüzde olarak kalıtılıyor ve her element kendi font boyutuna göre çözüyor: span 2.4px alıyor, genişlik 7.2px artıyor. Yüzdeli letter-spacing browser-compat-data'ya göre Chrome 145 (Şubat 2026), Firefox 133 ve Safari 17.4'ten beri üç motorda da var; web-features 3.46 bu alt özelliği ayrı bir Baseline kaydı olarak listelemiyor.

Pratik kural: letter-spacing'i px ile verme. Font büyüdüğünde aralık aynı kalır ve büyük başlıklarda neredeyse hiç görünmez, küçük metinde de fazla gelir. Etiketin kendisinde em kullanmak yeterli; iç içe font boyutları olan bir component'te yüzdeyi tercih et. Bookstore'daki etiket:

.badge {
    display: inline-block;
    padding: 0.125rem 0.5rem;
    border-radius: 0.25rem;
    background: var(--color-accent);
    color: #ffffff;
    font-size: 0.75rem;
    font-weight: 700;
    /* px değil em: badge'in kendi font-size'ıyla büyür */
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

Katalog sayfasında etiketin hesaplanan değeri 0.96px (12px'in %8'i). word-spacing aynı kurallara uyar ama gündelik arayüzde nadiren gerekir; en sık kullanıldığı yer, harf aralığı açılmış büyük harfli bir başlıkta kelimeler arasının harf aralığıyla karışmaması.

text-transform, text-align ve text-indent

Etiketin içindeki metin markup'ta "New" yazıyor; büyük harfe text-transform: uppercase çeviriyor. Bu dönüşüm element'in lang değerine göre yapılır:

<span lang="en" style="text-transform: uppercase">yeni kitap</span> /
<span lang="tr" style="text-transform: uppercase">yeni kitap</span> /
<span lang="tr" style="text-transform: lowercase">ISBN İADE</span>
lang=en uppercase  → YENI KITAP
lang=tr uppercase  → YENİ KİTAP
lang=tr lowercase  → ısbn iade

lang="en" ile "yeni" kelimesi YENI oluyor, lang="tr" ile doğru şekilde YENİ. Ters yönde de aynı kural geçerli: Türkçe sayfada lowercase, ISBN (International Standard Book Number; kitapların uluslararası kimlik numarası) kısaltmasını ısbn yapıyor. Türkçe arayüzde <html lang="tr"> zorunlu; kod, kısaltma ve marka adı gibi dönüşmemesi gereken parçalara ise text-transform uygulama ya da o parçaya kendi lang değerini ver. Büyük harf kullanımının ne zaman tercih edileceği UI/UX Tasarım İlkeleri serisinin Typography yazısında tartışılıyor; burada yalnız mekanizmayı görüyoruz. Ölçüm script'i metni innerText ile okuyor, çünkü innerText text-transform'u uygulanmış hâliyle döndürür; textContent ise markup'taki metni verir.

text-align'da fiziksel left/right yerine mantıksal start/end kullanmak, sayfa sağdan sola bir dile çevrildiğinde hizalamanın kendiliğinden dönmesini sağlar. text-align: justify yazdığında son satır yine başa yaslanır; bunu text-align-last belirler. text-indent ilk satırı içeri alır, hanging anahtar kelimesiyle de tersine çevrilir:

.indent { inline-size: 16em; text-indent: 2em; }
.indent.hanging { text-indent: 2em hanging; }
indent           line 1 starts at 32.0px, line 2 at 0.0px
indent hanging   line 1 starts at 0.0px, line 2 at 32.0px

Normal girintide ilk satır 32px (2em) içeriden başlıyor. hanging ile ilk satır kenarda kalıyor, sonraki satırlar içeri giriyor; kaynakça listeleri ve kitap künyesi için kullanışlı. hanging ve each-line anahtar kelimeleri Baseline newly available (Mart 2026, Chrome 146 ile).

text-shadow: Syntax, Çoklu Gölge ve Sınırları

text-shadow yatay ve dikey kayma, isteğe bağlı bulanıklık ve renkten oluşur; box-shadow'dan farklı olarak spread ve inset yoktur. Virgülle birden fazla gölge yazılabilir ve ilk yazılan en üstte çizilir:

.shadow { text-shadow: 0 1px 2px rgb(0 0 0 / 0.6), 0 0 8px #fbbf24; }
text-shadow: rgba(0, 0, 0, 0.6) 0px 1px 2px, rgb(251, 191, 36) 0px 0px 8px

Computed değerde renk başa alınmış ve uzunluklar px'e çevrilmiş; DevTools'ta gördüğün biçim bu. İlk gölge harflerin altına ince ve koyu bir gölge koyarak metni arka plandan ayırıyor, ikincisi sıfır kaymalı ve bulanık sarı bir "glow". Görsel bir banner üzerinde metni okunur kılmanın asıl yolu ise arka plana koyu bir gradient katmanı eklemek; bunu dördüncü yazıda kampanya banner'ında yapacağız. text-shadow'u yeterli contrast'ı sağlamak için kullanma: WCAG'nin (Web Content Accessibility Guidelines; web erişilebilirlik kuralları) contrast oranı yalnız metin ve arka plan rengiyle tanımlanır, otomatik araçların çoğu gölgeyi ya hiç hesaba katmaz ya da sonucu belirsiz sayar; ince bir gölge ise düşük çözünürlüklü ekranlarda metni daha da bulanık gösterir.

vertical-align: İkon Hizası ve img Altındaki Boşluk

vertical-align yalnız inline ve tablo hücresi seviyesindeki kutulara uygulanır; bir block'u ortalamak için kullanılmaz (bunun yolu Flexbox ya da Grid, sitedeki CSS serisinin (CSS3: Flexbox, Grid ve Modern CSS) 05 ve 06. yazıları). Inline bir ikon ya da görselde ise en sık iki sorunun kaynağıdır. Birincisi, bir div içindeki svg ya da img'nin altındaki açıklanamayan boşluk:

.icon-box { inline-size: max-content; background: #eee; }
.icon-box svg { inline-size: 24px; block-size: 24px; }
.icon-box.block svg { display: block; }
.icon-box.middle svg { vertical-align: middle; }
--- chromium
icon-box         box height 31px (svg 24px)
icon-box block   box height 24px (svg 24px)
icon-box middle  box height 25.140625px (svg 24px)
--- webkit
icon-box         box height 30.40625px (svg 24px)
icon-box block   box height 24px (svg 24px)
icon-box middle  box height 25.71875px (svg 24px)

24px'lik ikonun kutusu 31px yüksekliğinde. Sebep, inline bir görselin varsayılan olarak metnin baseline'ına oturması: altında, aynı satırda olabilecek g, p, y gibi harflerin aşağı uzanan kısmı için yer ayrılır. vertical-align: middle boşluğu küçültüyor ama sıfırlamıyor (25.1px); görsel tek başınaysa doğru çözüm display: block. Görsel reset'inin geri kalanı (max-inline-size, block-size: auto) bu serinin görseller yazısında.

İkinci sorun, fiyatın yanındaki ikonun metne göre yukarıda ya da aşağıda durması. 20px font ile dört değeri denedim; değerler ikonun üst ve alt kenarının baseline'a uzaklığı:

.price { font: 700 20px/1.5 system-ui, sans-serif; }
.price svg { inline-size: 1em; block-size: 1em; }
.price .baseline svg { vertical-align: baseline; }
.price .middle svg { vertical-align: middle; }
.price .text-bottom svg { vertical-align: text-bottom; }
.price .em svg { vertical-align: -0.125em; }
.probe { display: inline-block; inline-size: 0; block-size: 0; }
baseline         icon top -20.0px  bottom 0.0px from baseline
middle           icon top -15.3px  bottom 4.7px from baseline
text-bottom      icon top -16.0px  bottom 4.0px from baseline
em               icon top -17.5px  bottom 2.5px from baseline

baseline'da ikonun alt kenarı baseline'da, yani ikon rakamlardan daha yukarı uzanıyor. middle, ikonun ortasını baseline'ın x yüksekliğinin yarısı kadar yukarısına hizalar; rakamlar x yüksekliğinden uzun olduğu için ikon biraz aşağıda kalır. text-bottom alt kenarı font'un en alt noktasına indirir. -0.125em ise 1em'lik ikonu 2.5px aşağı kaydırıp ortasını baseline'ın 7.5px üstüne getirir. Bookstore'un katalog kartında aynı değeri kullandık:

.price svg {
    inline-size: 1em;
    block-size: 1em;
    /* 1em'lik ikonu rakamların ortasına hizalar */
    vertical-align: -0.125em;
    fill: currentColor;
}

Yukarıdaki 02-pages.mjs çıktısında ikonun ortası baseline'ın 6.0px, "420" rakamlarının görünen ortası 5.8px üstünde; fark 0.2px. Rakamların yüksekliğini canvas'ın measureText method'u ile aynı font'tan okudum. -0.125em her font için doğru değil; bu ölçümde system-ui'nin rakam yüksekliği yaklaşık 0.73em olduğu için ortaya yakın düşüyor; farklı bir font'ta aynı ölçümle değeri yeniden kontrol et. Tablo hücrelerinde ise vertical-align: middle beklendiği gibi hücre içeriğini dikeyde ortalar; tablo stilleri serinin son yazısında.

::first-letter ve initial-letter

::first-letter bir block'un ilk harfini seçer ve font, renk, margin gibi property'leri kabul eder. Kitap detayındaki özet paragrafının ilk harfini büyük bir başlık harfine çeviriyoruz:

.book-summary::first-letter {
    font-family: var(--font-display);
    font-weight: 700;
    color: var(--color-accent);
}
@supports (initial-letter: 2) {
    .book-summary::first-letter {
        initial-letter: 2;
        margin-inline-end: 0.5rem;
    }
}

initial-letter: 2 harfi iki satır yüksekliğinde büyütüp satırların içine oturtur; elle float ve font-size hesabı yapmaya gerek kalmaz. Ama Baseline değil: Chrome 110'dan beri prefix'siz var, Safari yalnız -webkit-initial-letter ile ve kısmen destekliyor, Firefox'ta yok. Ölçümde Chromium prefix'siz değeri, WebKit yalnız prefix'li değeri kabul etti. Bu yüzden @supports içine koyduk: desteklemeyen tarayıcı ilk harfi renkli ve kalın gösteriyor ama satırları bozmuyor.

Üretimde Dikkat

Bu ailenin property'leri ucuz: layout'u değiştirmeyen text-decoration-thickness, text-decoration-color ve text-shadow yalnız repaint gerektirir. letter-spacing, word-spacing ve text-transform ise metnin genişliğini değiştirdiği için layout tetikler; hover'da animasyonla değiştirmek satırların kaymasına yol açar. Animasyonu ölçmek ve compositor dostu property'leri seçmek CSS3 serisinin 13. yazısında anlatılıyor.

text-transform yalnız görünümü değiştirir. Büyük harfe çevrilmiş metnin ekran okuyucuda nasıl seslendirildiği ve kopyalanınca hangi hâliyle panoya gittiği tarayıcıya ve ekran okuyucuya göre değişir. Bir veriyi (ISBN, kupon kodu) büyük harfle göstermen gerekiyorsa veriyi kaynağında büyük harfle üret, CSS'e bırakma.

Link ve ikon hizası gibi görsel kararları otomatik test etmek zor görünür, ama bu yazıdaki script'ler gibi computed değerleri ve kutu ölçülerini okuyan küçük bir Playwright testi, bir refactoring'in hover'da kutuyu kaydırıp kaydırmadığını yakalar.

Sık Düşülen Tuzaklar

Birincisi, letter-spacing'i px ile vermek. Font büyüyünce aralık aynı kalır; em ile verip de iç içe farklı font boyutları olan bir component'te kalıtılan px değerine takılmak da aynı hatanın ikinci hâli. Ölçtüğümüz gibi yüzde bu iki sorunu da çözer.

İkincisi, okunmayan bir metni text-shadow ile kurtarmaya çalışmak. Contrast oranı metin ve arka plan rengiyle hesaplanır; gölge bu hesaba girmez. Arka planı değiştir ya da metnin arkasına yarı saydam bir katman koy.

Üçüncüsü, link'lerin alt çizgisini tamamen kaldırıp onları yalnız renkle ayırmak. Kart başlığında yaptığımız gibi alt çizgiyi silik bir renkte tutmak, link olduğunu belli ederken görsel olarak hafif kalır.

Dördüncüsü, hover'da border-bottom eklemek ya da font-weight'i değiştirmek. İkisi de kutunun boyutunu değiştirir ve komşu metni iter; text-decoration-thickness ve text-decoration-color bunu yapmaz.

Baseline Durumu

node scripts/baseline.mjs text-decoration text-decoration-skip-ink text-underline-offset letter-spacing text-shadow vertical-align text-transform text-align-last text-indent-hanging first-letter initial-letter
text-decoration            widely 2018-01-29  Chrome 1, Firefox 1, Safari 1, iOS 1
text-decoration-skip-ink   widely 2024-09-14  Chrome 64, Firefox 70, Safari 15.4, iOS 15.4
text-underline-offset      widely 2023-05-19  Chrome 87, Firefox 70, Safari 12.1, iOS 12.2
letter-spacing             widely 2018-01-29  Chrome 1, Firefox 1, Safari 1, iOS 1
text-shadow                widely 2018-01-29  Chrome 2, Firefox 3.5, Safari 1.1, iOS 1
vertical-align             widely 2018-01-29  Chrome 1, Firefox 1, Safari 1, iOS 1
text-transform             widely 2018-01-29  Chrome 1, Firefox 1, Safari 1, iOS 1
text-align-last            widely 2025-03-12  Chrome 47, Firefox 49, Safari 16, iOS 16
text-indent-hanging        newly 2026-03-13   Chrome 146, Firefox 121, Safari 15, iOS 15
first-letter               widely 2018-01-29  Chrome 1, Firefox 1, Safari 1, iOS 1
initial-letter             not Baseline       Chrome 110, Firefox -, Safari -, iOS -
ÖzellikDurumNot
text-decoration longhand'leri, text-underline-offset, text-decoration-skip-inkWidely availableShorthand'in kalınlığı içermesi Safari'de 26.2'den beri
letter-spacing (uzunluk)Widely availableYüzde değer: Chrome 145, Firefox 133, Safari 17.4
text-shadow, vertical-align, text-transformWidely availabletext-transform lang'a göre çalışır
text-align-lastWidely available2025-03'ten beri
text-indent ile hanging ve each-lineNewly available2026-03'ten beri
initial-letterBaseline değilChrome; Safari yalnız -webkit- ile; @supports içinde kullan

Küçük Bir Deneme

★☆☆ Katalogdaki etiketin letter-spacing'ini 8% yap ve etiketin içine 16px'lik bir span ekle. 02-pages.mjs'e span'in computed değerini yazdıran bir satır ekleyip ölç.

★★☆ Fiyat ikonunu Fraunces ile yazılmış bir başlığın yanına koy (örneğin kitap detayındaki h1'in yanına bir favori ikonu). 02-pages.mjs'teki ölçümü bu başlık için tekrarla ve -0.125em'in bu font için doğru değer olup olmadığını bul.

★★★ initial-letter için Safari'nin prefix'li değerini de kapsayan ikinci bir @supports bloğu yaz ve WebKit'te ilk harfin iki satır yüksekliğe oturduğunu, harfin ve paragrafın kutularını ölçerek doğrula.

Bir sonraki yazıda metnin nerede ve nasıl kırıldığına bakıyoruz: white-space ve longhand'leri, uzun ISBN ve URL'ler için overflow-wrap ile word-break farkı, hyphens'ın Türkçe'deki durumu, text-wrap: balance ve pretty, ve kitap açıklamasını üç satırda kesen line clamp.