Mert Özen Her Satırda Daha İleri
Frontend UI/UX Tasarım İlkeleri — #03

Typography: Type Scale, Line Height, Satır Uzunluğu, Font Seçimi ve Türkçe Karakterler

Mert Özen 14 Kas 2026 12 dk 7 görüntülenme
Typography: Type Scale, Line Height, Satır Uzunluğu, Font Seçimi ve Türkçe Karakterler

İyi typography birkaç kurala dayanır: tek oranlı bir type scale, satır başına 45–75 karakter, gövdede 1.5–1.6 line-height ve tabloda tabular rakamlar. Bookstore'un kitap sayfasında 146 karakterlik satırları ve kayan ondalık noktaları ölçüp düzeltiyor, ch biriminin Inter'deki tuzağını, latin subset'inde eksik kalan ğ, ş, İ ve ₺'yi ve lang olmadan büyük harfe çevrilen i'yi Chromium'da gösteriyoruz.

Sürüm notu: Bu yazıdaki örnek ekranlar Playwright 1.65 ile Chromium 158'de render edilip ölçülmüştür; script'ler Node.js 24 ile çalıştırılmıştır. Font: Inter, @fontsource-variable/inter 5.3 paketinden.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için bookstore-web'in kitap sayfasında "About this book" bölümü ve baskıların fiyat tablosu var. Sayfanın stili şöyle:

body { color: #222; font-size: 15px; }
.page { max-width: 1100px; margin: 0 auto; padding: 24px; }
.title { font-size: 23px; text-transform: uppercase; margin: 0; }
.author { font-size: 17px; margin: 4px 0 24px; }
.section { font-size: 19px; margin: 24px 0 8px; }
.desc { font-size: 15px; }
.editions { border-collapse: collapse; font-size: 14px; }
.editions td { padding: 4px 24px 4px 0; }
.price { text-align: right; }

Üç soru. 1280 piksel genişlikte bir ekranda açıklama paragrafının bir satırında kaç karakter olur? Fiyat sütunu sağa hizalı; ₺189.90, ₺418.00, ₺1,111.00 ve ₺111.10'un ondalık noktaları alt alta mı durur? Son soru Türkçe katalog verisiyle ilgili: aynı text-transform: uppercase, "bilim kurgu" kategori adını İngilizce bir sayfada nasıl yazar?

Bir önceki yazıda kitap detay sayfasının hiyerarşisini boyut, ağırlık ve renkle kurduk ama değerleri sabit sayılar olarak bıraktık. Bu yazıda typography'yi bir sisteme bağlıyoruz: type scale, satır uzunluğu, line-height, font seçimi, rakamlar ve Türkçe karakterler.

Type Scale: Az Boyut, Bir Oran

Before sayfasında beş ayrı font boyutu var: 14, 15, 17, 19 ve 23 piksel. Hiçbiri bir kurala bağlı değil; her biri yazıldığı gün "biraz büyük olsun" denerek seçilmiş. Sorun yalnız estetik değil: 15 ile 17 piksel arasındaki fark göze hiyerarşi olarak değil, tutarsızlık olarak görünüyor ve bir sonraki geliştirici 16 ya da 18'i de ekliyor.

Modular type scale bu sorunu bir taban boyut ve bir oranla çözüyor: her adım bir öncekinin oran katı. Yaygın oranlar 1.2 (minor third), 1.25 (major third) ve 1.333 (perfect fourth); adları müzikteki aralıklardan geliyor. 16 piksel tabanla hesaplayalım:

// Modular type scale: base * ratio^step, step -1..4
const base = 16;
for (const [name, ratio] of [['minor third', 1.2], ['major third', 1.25], ['perfect fourth', 1.333]]) {
  const steps = [-1, 0, 1, 2, 3, 4].map((s) => base * ratio ** s);
  console.log(`${name.padEnd(15)} ${ratio}: ${steps.map((v) => v.toFixed(1)).join(' / ')}  ->  ${steps.map((v) => Math.round(v)).join(' / ')}`);
}
minor third     1.2: 13.3 / 16.0 / 19.2 / 23.0 / 27.6 / 33.2  ->  13 / 16 / 19 / 23 / 28 / 33
major third     1.25: 12.8 / 16.0 / 20.0 / 25.0 / 31.3 / 39.1  ->  13 / 16 / 20 / 25 / 31 / 39
perfect fourth  1.333: 12.0 / 16.0 / 21.3 / 28.4 / 37.9 / 50.5  ->  12 / 16 / 21 / 28 / 38 / 51

Oran büyüdükçe başlıklar hızla büyüyor. 1.333'te dördüncü adım 51 piksel; bir e-ticaret sayfası için fazla. 1.2'de adımlar birbirine çok yakın, 16 ile 19 arasındaki fark zayıf. Bookstore için 1.25'i seçtim: 13, 16, 20, 25, 31, 39. Bu altı değer, kitap sayfasındaki her metnin karşılığını bulmaya yetiyor. Oran bir başlangıç noktası; hesaplanan değeri tam piksele yuvarlamak ve gerekirse bir adımı elle düzeltmek normal.

Değerler token olarak tek bir dosyada duruyor. Bu dosya serinin ilerleyen yazılarında renk ve spacing token'larıyla büyüyecek:

/* Bookstore design token'ları. 03: typography */
:root {
  --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* type scale: 16px taban, 1.25 oran, tam piksele yuvarlanmış */
  --text-sm: 0.8125rem;  /* 13px */
  --text-base: 1rem;     /* 16px */
  --text-lg: 1.25rem;    /* 20px */
  --text-xl: 1.5625rem;  /* 25px */
  --text-2xl: 1.9375rem; /* 31px */
  --text-3xl: 2.4375rem; /* 39px */

  --weight-regular: 400;
  --weight-semibold: 600;
  --weight-bold: 700;

  --leading-tight: 1.2;  /* başlıklar */
  --leading-normal: 1.5; /* arayüz metni */
  --leading-prose: 1.6;  /* uzun okuma metni */

  --measure: 52ch;       /* Inter gövde metninde yaklaşık 66 karakter */
}

Değerler rem; kullanıcı tarayıcısında varsayılan font boyutunu büyüttüğünde bütün skala onunla birlikte büyüyor. Ağırlıklar da üçle sınırlı. Ekran genişliğine göre akıcı büyüyen başlıklar için clamp() kullanmak bu skalanın üstüne eklenebilir; mekaniği "CSS3: Flexbox, Grid ve Modern CSS" serisinin responsive design ve fluid typography yazısında anlatıldı.

Satır Uzunluğu ve Line-height

Okunabilirlikte en çok gözden kaçan ayar satır uzunluğu. Satır çok uzunsa göz bir satırın sonundan bir sonrakinin başına dönerken yerini kaybediyor; çok kısaysa sürekli satır atlamak yoruyor. web.dev'in Learn Design kursu, araştırmalara dayanarak satır başına 45 ile 75 karakteri rahat, 66 civarını ideal aralık olarak veriyor. WCAG'nin (Web Content Accessibility Guidelines; web erişilebilirlik standardı) AAA (üç uygunluk seviyesinin en sıkısı) seviyesindeki 1.4.8 kriteri de metin bloklarının satır genişliğinin 80 karakteri geçmemesini istiyor.

Ölçüm script'i, açıklama paragrafının her karakteri için bir Range oluşturup ekrandaki satırını buluyor ve satır başına karakter sayısını hesaplıyor:

node scripts/03-typography.mjs
--- before @1280px: font sizes 14/15/17/19/23
  description: 15px, line-height normal, 4 lines, avg 146 chars/line, max 148
  price decimal points x: 291.8, 291.9, 291.9, 295.9 (spread 4.1px)
--- after @1280px: font sizes 13/16/20/39
  description: 16px, line-height 25.6px, 8 lines, avg 66 chars/line, max 68
  price decimal points x: 330.4, 330.4, 330.4, 330.4 (spread 0.0px)
--- "Inter Latin Only": web font 70 glyphs, system fallback 5 glyphs
--- Inter: web font 75 glyphs, system fallback 0 glyphs
--- not in the latin subset: ş İ ğ ₺
--- uppercase lang="en": BILIM KURGU · ŞIIR · ÇIZGI ROMAN
--- uppercase lang="tr": BİLİM KURGU · ŞİİR · ÇİZGİ ROMAN

İlk sorunun cevabı: before'da satır başına ortalama 146 karakter, ideal aralığın iki katı. After'da 66.

Burada bir tuzak var. Satır uzunluğu için genellikle max-inline-size: 66ch önerilir. ch birimi ise font'taki "0" karakterinin genişliği. Inter'de 16 pikselde "0" 10.1 piksel, İngilizce metindeki ortalama karakter ise bundan dar. İlk denemede 66ch ile ölçtüğümde satır başına ortalama 85 karakter çıktı; 52ch ile 66 karaktere indi. Yani ch değerini font'tan bağımsız bir "karakter sayısı" sanma, kendi font'unla ölç. Token'daki 52ch bu ölçümün sonucu.

Line-height uzun metinde nefes payı veriyor. Before'daki normal değeri Inter için yaklaşık 1.2; 15 piksellik ve 146 karakterlik satırlarda satırlar birbirine yapışıyor. Gövde metni için 1.5 ile 1.6 arası iyi bir başlangıç; satır uzadıkça daha fazla aralık gerekir. Başlıklarda ise tersi geçerli: büyük boyutta 1.5 satır aralığı başlığın iki satırını birbirinden kopuk gösterir, bu yüzden başlıklar 1.2 civarında. Değerleri birimsiz yazmak önemli; 1.6 font boyutuyla birlikte ölçeklenir, 24px ölçeklenmez.

WCAG 2.2'nin AA (çoğu yasal yükümlülüğün istediği orta seviye) seviyesindeki 1.4.12 Text Spacing kriteri de tasarımı etkiliyor: kullanıcı line-height'i font boyutunun 1.5 katına, paragraf aralığını 2 katına, harf aralığını 0.12 ve kelime aralığını 0.16 katına çıkardığında içerik kesilmemeli ve üst üste binmemeli. Sabit yükseklikli kartlar ve overflow: hidden ile kırpılmış başlıklar bu testte en sık düşen yerler.

Font Seçimi: Sistem Font'u mu, Web Font mu?

İki seçenek var ve ikisinin de bedeli farklı.

Sistem font stack'i (system-ui, Windows'ta Segoe UI, Android'de Roboto) indirme gerektirmiyor, sayfa ilk anda doğru font'la açılıyor ve kullanıcının işletim sistemindeki metinle aynı görünüyor. Bedeli, her platformda farklı bir font görünmesi: ölçüler, satır kırılmaları ve karakter genişlikleri değişiyor; az önceki ch hesabı her sistemde ayrı sonuç verir.

Web font markaya bir ses veriyor ve her platformda aynı metrikleri sağlıyor. Bedeli indirme süresi ve font gelene kadar ne gösterileceği kararı. Bookstore için Inter'i seçtim: açık lisanslı (OFL), rakamları ve Türkçe karakterleri iyi destekliyor, variable font olarak tek dosyada 100 ile 900 arası bütün ağırlıkları taşıyor. @fontsource-variable/inter paketinde latin subset dosyası 48.256, latin-ext dosyası 85.068 bayt. Variable font, üç ayrı ağırlık için üç ayrı dosya indirmekten genellikle daha ucuz.

Font yüklemenin performans tarafı (font-display, preload, layout shift) bu yazının konusu değil; ilerideki "Web Performansı" serisinin görsel ve font optimizasyonu yazısında, Türkçe subset'lerle birlikte ele alınacak. Tasarım açısından kural şu: kaç font ailesi kullanacağına baştan karar ver. Çoğu ürün için bir aile yeter; başlık ve gövde için iki aile üst sınır.

Rakamlar: Tabular Figures

İkinci sorunun cevabı ölçümde: before'da fiyat sütunu sağa hizalı olduğu hâlde ondalık noktaların x konumları 291.8 ile 295.9 piksel arasında, 4.1 piksel kayıyor. Sebep proportional rakamlar: Inter'de varsayılan olarak "1", "0"dan daha dar. ₺111.10'daki "10" ile ₺418.00'daki "00" farklı genişlikte olduğu için nokta yer değiştiriyor ve göz sütunu tararken basamakları karşılaştıramıyor.

Çözüm tek satır: font-variant-numeric: tabular-nums bütün rakamları aynı genişlikte çizer. After'da dört noktanın dördü de 330.4 pikselde. Fiyat listeleri, sepet toplamları, sipariş tabloları ve sayaçlar gibi değeri değişen ya da alt alta karşılaştırılan her sayıda tabular rakam kullan. Paragraf içindeki tek bir sayı için gerekmez; orada proportional rakamlar metinle daha iyi akar. Font bu özelliği desteklemiyorsa CSS bir şey yapamaz, bu yüzden font seçerken rakamlara da bak.

Türkçe Karakter Tuzakları

Font subset'inde eksik harfler

Web font'lar genellikle dil gruplarına göre subset'lere bölünür; tarayıcı yalnız sayfadaki karakterlerin düştüğü dosyayı indirir. Bir projede "dosya küçük olsun" diye yalnız latin subset'inin bırakıldığını düşün. Paketin unicode.json dosyasındaki aralıklara göre latin subset'i Ç, Ö, Ü ve noktasız ı'yı içeriyor, ama ğ, ş, İ ve ₺ latin-ext'te. Bunu bir lab sayfasında, yalnız latin dosyasını yükleyen bir @font-face ile denedim:

<style>
  /* yalnız latin subset'i, latin-ext yok */
  @font-face {
    font-family: "Inter Latin Only";
    font-weight: 100 900;
    src: url("/node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2") format("woff2");
  }
  .latin-only { font-family: "Inter Latin Only", serif; }
  .full { font-family: "Inter", serif; }
  .label { text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
</style>
<p class="latin-only" id="a1" lang="tr">Çalıkuşu · İnce Memed · Ağrı Dağı Efsanesi · Kürk Mantolu Madonna · ₺420.00</p>
<p class="full" id="a2" lang="tr">Çalıkuşu · İnce Memed · Ağrı Dağı Efsanesi · Kürk Mantolu Madonna · ₺420.00</p>
<p class="full label" id="b1" lang="en">bilim kurgu · şiir · çizgi roman</p>
<p class="full label" id="b2" lang="tr">bilim kurgu · şiir · çizgi roman</p>

Script, Chrome DevTools Protocol'ün CSS.getPlatformFontsForNode komutuyla her paragrafta hangi glyph'in hangi font'la çizildiğini soruyor. Çıktıya göre yalnız latin subset'iyle beş glyph (ş, İ, iki ğ ve ₺) sistemin fallback font'una düşüyor; iki subset birlikte yüklendiğinde fallback sıfır. Ekranda bunun karşılığı, kelimenin ortasında başka bir font'tan gelen harf:

03-turkish-glyphs-tr.png

Birinci satırda İ, ğ, ş ve ₺ serif bir font'tan geliyor; Windows'ta fallback font'u farklı olur ama sorun aynı. İngilizce arayüzlü bir sayfa bile Türkçe katalog verisi ve ₺ işareti gösteriyorsa latin-ext'e ihtiyaç duyar. Font seçerken ve subset ayarlarken Türkçe bir test cümlesiyle bak; yalnız "Lorem ipsum" ile kontrol edilen font bu sorunu göstermez.

text-transform ve lang

Üçüncü sorunun cevabı da çıktıda. text-transform: uppercase büyük harf dönüşümünü sayfanın diline göre yapar. lang="en" olan bir element'te "bilim kurgu" BILIM KURGU oluyor: İngilizcede i'nin büyüğü I. Türkçede ise i'nin büyüğü İ, ı'nın büyüğü I; lang="tr" verildiğinde sonuç BİLİM KURGU. MDN (Mozilla'nın web dokümantasyonu) de dönüşümün dile özgü kurallara uyduğunu ve dilin lang attribute'undan alındığını belirtiyor.

Pratik sonuç: Türkçe arayüzde <html lang="tr"> şart. Arayüz İngilizce ama içerik Türkçeyse (Bookstore'daki kitap ve kategori adları gibi), Türkçe veriyi taşıyan element'e lang="tr" ver. Aynı kural JavaScript'te toUpperCase() yerine toLocaleUpperCase('tr-TR') kullanmak için de geçerli.

Büyük harfle yazılan başlıkta harf aralığı

Küçük harflerin üst ve alt uzantıları kelimeye bir şekil verir; büyük harfle yazılan metinde bu şekil kaybolur ve harfler birbirine yakın durur. Bu yüzden büyük harfli kısa etiketlerde harf aralığını biraz açmak (after'da letter-spacing: 0.06em) okumayı kolaylaştırır. Uzun metni hiç büyük harfle yazma; başlıklarda da sentence case daha kolay okunur. Before'daki "MIDDLEMARCH" başlığı after'da "Middlemarch" oldu; büyük harf yalnız küçük bölüm etiketlerinde kaldı.

Kitap Sayfası: Önce ve Sonra

After stili bütün değerleri token'lardan alıyor:

body { color: #222; font-family: var(--font-sans); font-size: var(--text-base); line-height: var(--leading-normal); }
.page { max-width: 1100px; margin: 0 auto; padding: 24px; }
.title { font-size: var(--text-3xl); line-height: var(--leading-tight); margin: 0; }
.author { font-size: var(--text-lg); margin: 4px 0 24px; }
.section {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 32px 0 8px;
}
.desc { max-inline-size: var(--measure); line-height: var(--leading-prose); margin: 0; }
.editions { border-collapse: collapse; }
.editions td { padding: 4px 24px 4px 0; }
.price { text-align: right; font-variant-numeric: tabular-nums; }

03-typography-before-after-tr.png

Font boyutu sayısı beşten dörde indi (13, 16, 20, 39) ve dördü de skaladan. Açıklama 8 satıra çıktı ama her satır tek bakışta okunabilir uzunlukta. Bölüm başlıkları küçüldü ve büyük harfe döndü: bu sayfada kitap adı tek büyük başlık, "About this book" ise içeriği gruplayan bir etiket. Bir önceki yazıdaki hiyerarşi kuralının typography karşılığı bu.

Üretimde Dikkat

Type scale'i kodda tek yerde tut ve component'lerde sabit piksel yazılmasını code review'da yakala. Bu yazıdaki style audit script'i, sayfada skalada olmayan bir font boyutu görürse CI'da uyarı verecek şekilde genişletilebilir. Tailwind kullanan projelerde aynı skala @theme içinde tanımlanır; bunun ayrıntısı ilerideki "Tailwind CSS" serisinin theme ve design token yazısında.

Gerçek içerikle test et. Kitap adları kısa da olur ("Emma") uzun da ("Saatleri Ayarlama Enstitüsü"); 39 piksellik başlık dar ekranda üç satıra çıkabilir. Tasarımı en uzun gerçek veriyle ve Türkçe karakterli veriyle render et, sonra kararı ver.

Kullanıcının font boyutu tercihine saygı göster. Tarayıcıda varsayılan boyutu 20 piksele çıkaran bir kullanıcı için rem tabanlı skala orantılı büyür; köke font-size: 62.5% gibi hilelerle ya da piksel değerlerle bu tercihi bozma.

Sık Düşülen Tuzaklar

Birincisi, her ihtiyaçta yeni bir font boyutu eklemek. Skalada olmayan 15 ya da 17 piksel, sistemin dağılmaya başladığının ilk işareti.

İkincisi, satır uzunluğunu sınırlamamak. Geniş ekranda tam genişlik akan paragraf, ölçtüğümüz gibi 146 karakterlik satırlar üretiyor.

Üçüncüsü, ch'yi font'tan bağımsız sanmak. 66ch, Inter'de yaklaşık 85 karakter; değeri kendi font'unla ölç.

Dördüncüsü, fiyat ve tablo rakamlarında proportional rakam bırakmak. Sağa hizalı sütun bile tabular rakam olmadan hizalanmıyor.

Beşincisi, Türkçeyi test etmemek: eksik subset'te fallback'e düşen ğ, ş, İ ve ₺, lang verilmeden yapılan büyük harf dönüşümünde I olan i.

Küçük Bir Deneme

★☆☆ tokens.css'te --measure'ı 66ch yap ve 03-typography.mjs'i çalıştır. Satır başına ortalama karakter sayısının ne olacağını önce tahmin et, sonra ölç.

★★☆ turkish.html'e lang="en" ile text-transform: lowercase uygulanan KIRMIZI KEDİ metnini ekle. Sonucu tahmin et, sonra aynı metni lang="tr" ile karşılaştır ve script'e bir satır ekleyerek innerText değerlerini yazdır.

★★★ Type scale'i 1.2 oranıyla yeniden kur, kitap sayfasını ve bir önceki yazının kitap detay sayfasını bu skalayla render et. 1.25 ile karşılaştırınca başlık ve fiyat arasındaki farkın hâlâ yeterli olup olmadığını squint test ve style audit çıktısıyla değerlendir.

Bir sonraki yazıda renk var: marka rengi, nötr skala ve semantic renklerden oluşan bir palet kurmak, OKLCH (renkleri algılanan açıklığa göre tanımlayan renk uzayı) ile algısal olarak düzgün tonlar üretmek, WCAG 2.2 contrast oranlarını kendi script'imizle hesaplamak, APCA'nın (WCAG 3 için önerilmiş bir kontrast hesaplama yöntemi) bugünkü durumu ve dark mode paletini ters çevirmeden yeniden ayarlamak.