Metin Semantiği: Heading, Paragraph, List, Link, Button ve Inline Element'ler
Kullanıcıyı başka bir adrese götüren şey a, sayfada bir aksiyon yapan şey button'dır; href="#" ve tıklanabilir div klavyeyle ya hiç ya da yarım çalışır. Bu yazıda strong/em ile b/i farkını, dl ile kitap künyesini, time, abbr, code/kbd/samp, blockquote/q/cite ve figure'ı Chromium'un accessibility tree'sinde görüyor, link ile button farkını gerçek bir klavye testiyle ve target="_blank"'in implicit noopener davranışını iki motorda ölçüyoruz.
Sürüm notu: Bu yazı HTML (HyperText Markup Language) Living Standard'ın ve Baseline'ın Kasım 2026 durumuna göre yazıldı; örnekler Chrome 158, WebKit 27.2 (Playwright 1.65) ve Nu Html Checker 26.11.9 ile denenmiştir.
Önce Tahmin Et
Bookstore'un kitap detay sayfasında üç ayrı ekip üç ayrı "Add to cart" yazmış:
<a href="#" onclick="addToCart('a')">Add to cart</a>
<div class="btn" onclick="addToCart('div')">Add to cart</div>
<button type="button" onclick="addToCart('button')">Add to cart</button>
CSS (Cascading Style Sheets) ile üçü de aynı mavi butona benziyor ve fareyle üçü de çalışıyor. Fare kullanmayan biri Tab ile sayfada gezip Enter ya da Space'e bastığında hangileri sepete kitap ekler? Ve ilk satırdaki href="#" sayfanın adresine ne yapar?
Önceki yazıda sayfanın büyük bloklarını landmark'larla adlandırdık. Bu yazıda o blokların içine, metne iniyoruz: vurgu, kitap künyesi, alıntı, kod, ve en sık yanlış yapılan karar olan link ile button ayrımı. Her iddiayı Chromium'un accessibility tree'sinden ya da gerçek bir klavye testinden okuyacağız.
Vurgu Ailesi: strong, em, b, i, mark ve small
Hepsi metni kalın, eğik ya da renkli gösterir; farkları görünümde değil, ne söylediklerinde. Spec'in tanımlarını kendi cümlelerimle şöyle toparlıyorum:
strong önem, ciddiyet ya da aciliyet bildirir: "Bu kitap iade edilemez" cümlesindeki "iade edilemez" gibi. em ise yeri değişince cümlenin anlamı da değişen sesli vurgudur: "Bu kitap bir framework eğitimi değil" cümlesinde vurguyu "framework"e koymakla "değil"e koymak aynı şeyi söylemez; birincisi kitabın başka bir konunun eğitimi olduğunu, ikincisi "framework eğitimi sanıyorsan yanılıyorsun" itirazını ima eder. b ek bir önem katmadan dikkat çekmek içindir: bir özetteki anahtar kelimeler, bir yorumdaki ürün adı. i metinden farklı bir sesi işaretler: yabancı bir terim, bir düşünce, bir gemi adı. mark bulunduğu bağlamdaki ilgiyi vurgular; en tipik kullanımı arama sonuçlarında aranan kelimeyi işaretlemek. small yan not ve küçük puntolu yasal metin içindir: "Fiyatlara KDV (Katma Değer Vergisi) dahildir."
Pratikte iki kural yetiyor. Birincisi, sadece görünüm istiyorsan element değil CSS kullan: bir kitap kartında fiyatın kalın olması bir tasarım kararıdır, font-weight ile yazılır. İkincisi, strong'u "kalın yazı kısayolu" gibi her yere serpiştirme; her şey önemliyse hiçbir şey önemli değildir. Ekran okuyucuların bu vurguları sesli olarak ayrıca belirtmesine de güvenme; çoğu varsayılan ayarlarında bunu yapmıyor. Anlamı taşıyan cümlenin kendisi olmalı.
Kitap Künyesi: dl, dt, dd ve time
Kitap detay sayfasının en yapısal parçası künyedir: yazar, yayın tarihi, ISBN (International Standard Book Number), formatlar. Bunlar ad–değer çiftleri ve HTML'in bunun için bir element'i var: description list.
<dl>
<dt>Author</dt>
<dd>Robert C. Martin</dd>
<dt>Published</dt>
<dd><time datetime="2017-09-10">10 September 2017</time></dd>
<dt>ISBN</dt>
<dd>978-0134494166</dd>
<dt>Formats</dt>
<dd>Paperback</dd>
<dd>E-book</dd>
</dl>
dt adı, dd değeri taşır ve bir dt'nin birden fazla dd'si olabilir; "Formats" satırında olduğu gibi. Stil vermek için her çifti bir div ile sarmana da spec izin veriyor; grid ile iki kolonlu künye yapmak bu sayede kolay. Bu yapıyı tabloyla ya da <p><b>Author:</b> …</p> satırlarıyla da kurabilirdin; fark, ekran okuyucunun bunu terim–tanım çiftlerinden oluşan bir liste olarak sunabilmesi ve makinelerin hangi değerin hangi ada ait olduğunu bilmesi.
Tarih için time kullandım. Ekranda insanın okuyacağı biçim, datetime attribute'unda makinenin okuyacağı biçim duruyor. Türkçe sayfada ekrana "10 Eylül 2017" yazarsın, datetime yine 2017-09-10 kalır. Değer spec'in tanıdığı bir biçimde olmalı; datetime="10/09/2017" yazdığımda checker bunu reddetti:
:5.14-5.41: error: Bad value “10/09/2017” for attribute “datetime” on element “time”: The literal did not satisfy the time-datetime format.
Bu hata gerçek bir belirsizliği yakalıyor: 10/09 Ekim'in 9'u mu, Eylül'ün 10'u mu? ISO (International Organization for Standardization) 8601 biçimi bu soruyu ortadan kaldırıyor. Sunucu tarafında ASP.NET Core'dan bu değeri üretirken de ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) gibi kültürden bağımsız bir biçim kullan; Türkçe kültürle formatlanmış bir tarih buraya girmemeli.
Alıntı, Kısaltma, Kod ve Figure
Künyenin altındaki açıklama ve yorum bölümü metin semantiğinin geri kalanını gösteriyor:
<p>Run <code>dotnet new sln</code> and press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd> to build.
The terminal prints <samp>Build succeeded.</samp></p>
<blockquote cite="https://example.com/reviews/42">
<p>The chapters on boundaries changed how I split modules.</p>
</blockquote>
<p>The review title says <q>Worth reading twice</q> and compares the book with
<cite>Refactoring</cite>.</p>
<p><mark>In stock</mark> <small>Prices include VAT.</small></p>
<p>The <abbr title="Single Responsibility Principle">SRP</abbr> chapter is short.</p>
<figure>
<img src="cover.jpg" alt="Cover of Clean Architecture" width="200" height="300">
<figcaption>First edition cover</figcaption>
</figure>
code bir kod parçasını, kbd kullanıcının bastığı tuşu ya da yazdığı girdiyi, samp bir programın ürettiği çıktıyı işaretler. Birden fazla satırlık kod için pre içine code koyarsın; pre boşlukları ve satır sonlarını korur, code anlamı taşır. Teknik dokümantasyon yazan bir ekip için bu üçlü, "hangisi benim yazacağım, hangisi ekranda göreceğim" ayrımını metne gömüyor.
blockquote başka bir kaynaktan alınmış blok alıntıdır; cite attribute'u kaynağın URL'sini (Uniform Resource Locator) taşır ama kullanıcıya gösterilmez, görünmesini istiyorsan kaynağı ayrıca metne yaz. Satır içi kısa alıntı için q var; tırnak işaretlerini tarayıcı CSS ile kendisi ekler, sen yazmazsın. cite element'i ise bir eserin adı içindir: kitap, film, makale. Spec bir kişinin adını cite ile işaretlemeyi açıkça yanlış sayıyor; yazarın adı düz metindir.
abbr kısaltmayı işaretler ve title ile açılımını verebilir. Ama title'a güvenme: fareyle üzerine gelmeden görünmez, dokunmatik ekranda hiç görünmez ve ekran okuyucular onu tutarlı biçimde okumaz. Doğru yöntem bu sitenin yazılarında da yaptığımız gibi kısaltmayı ilk geçtiği yerde metnin içinde açmak; abbr bunun üstüne eklenen bir işarettir.
figure, ana metinden bahsedilen ama yeri değişse de anlamını koruyan kendi başına bir içeriktir: kitap kapağı, bir diyagram, bir kod örneği. figcaption onun başlığıdır ve figure'ın ilk ya da son çocuğu olmalıdır. Görselin alt'ı ile figcaption aynı şeyi tekrar etmemeli: alt görselde ne olduğunu, figcaption neden orada olduğunu söyler. Görsellerin ayrıntısı serinin image ve media yazısında.
Tarayıcı Bunları Nasıl Görüyor?
Bu element'lerin hepsini içeren kitap sayfasını, önceki yazıdaki script'in role filtresini gevşetilmiş bir sürümüyle Chromium'a verdim. generic, paragraph ve düz metin node'larını gizleyince kalan ağaç şu:
main
article
heading "Clean Architecture"
DescriptionList
term "Author"
definition
term "Published"
definition
time
term "ISBN"
definition
term "Formats"
definition
definition
strong
emphasis
code
blockquote
mark
Abbr "Single Responsibility Principle"
figure
image "Cover of Clean Architecture"
Figcaption
İki şey dikkat çekiyor. Birincisi, künye artık gerçekten bir yapı: term ve definition çiftleri, "Formats" altında iki definition. İkincisi, listede olmayanlar da bir şey söylüyor: b, i, small, kbd, samp, q ve cite Chromium'un ağacında ayrı bir role almıyor. Bu onların anlamsız olduğu demek değil; anlamları spec'te ve arama motorları, okuma görünümleri, çeviri araçları gibi metni işleyen yazılımlar için var. Ama yardımcı teknolojiye özel bir şey taşımıyorlar. Doğru element'i seçmek hiçbir zaman zarar vermez, yanlış element'i seçmek ise bir yerde mutlaka bir şeyi bozar. Bunun en net örneği bir sonraki bölümde.
Link mi Button mı?
Kuralı tek cümleye indirebiliriz: kullanıcıyı başka bir yere götürüyorsa a, sayfada bir şey yapıyorsa button. "View cart" bir adrese gider, link; "Add to cart" sepete kitap ekler, button. Tahmin sorusundaki üç varyantı bir "View cart" linkiyle aynı sayfaya koydum:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Book actions | Bookstore</title>
</head>
<body>
<a href="/cart">View cart</a>
<a href="#" onclick="addToCart('a')">Add to cart</a>
<div class="btn" onclick="addToCart('div')">Add to cart</div>
<button type="button" onclick="addToCart('button')">Add to cart</button>
<script>
const clicks = [];
function addToCart(source) {
clicks.push(source);
}
</script>
</body>
</html>
Bunu gerçek bir tarayıcıda klavyeyle denemek için bir Playwright script'i yazdım. Sayfayı küçük bir HTTP (Hypertext Transfer Protocol) sunucusundan servis ediyor, Tab ile gezip odaklanan element'i yazdırıyor, sonra Enter ve Space'e basıp hangi tıklamaların gerçekleştiğini sayıyor:
const { chromium } = require('playwright');
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(fs.readFileSync('actions.html'));
}).listen(18878);
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:18878/book/42');
// Tab ile hangi element'lere gidilebiliyor?
for (let i = 1; i <= 4; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluate(() => {
const el = document.activeElement;
return el === document.body ? 'body' : `${el.tagName.toLowerCase()} "${el.textContent}"`;
});
console.log(`Tab ${i}: ${focused}`);
}
// Enter ve Space ile tıklama tetikleniyor mu?
for (const selector of ['a[href="#"]', 'button']) {
for (const key of ['Enter', ' ']) {
await page.focus(selector);
await page.keyboard.press(key === ' ' ? 'Space' : key);
}
}
console.log('clicks:', await page.evaluate(() => clicks));
console.log('url:', page.url());
console.log('roles:', await page.getByRole('button', { name: 'Add to cart' }).count(), 'button,',
await page.getByRole('link', { name: 'Add to cart' }).count(), 'link');
await browser.close();
server.close();
})();
Tab 1: a "View cart"
Tab 2: a "Add to cart"
Tab 3: button "Add to cart"
Tab 4: body
clicks: [ 'a', 'button', 'button' ]
url: http://localhost:18878/book/42#
roles: 1 button, 1 link
Çıktıyı satır satır okuyalım. div Tab sırasına hiç girmedi: Tab 3'te odak doğrudan button'a geçti, Tab 4'te sayfadan çıktı. Klavye kullanıcısı için div'li "Add to cart" yok. href="#"'li link Tab ile bulunuyor ve Enter ile çalışıyor, ama Space ile çalışmıyor: clicks listesinde yalnız bir 'a' var. Link'te Space sayfayı kaydırır. Button ise hem Enter hem Space ile iki kez tetiklendi. Üstelik link adresi kirletti: URL'nin sonuna # eklendi, tarayıcı geçmişine yeni bir kayıt girdi ve sayfa en üste kaydırılmış olabilir. Son satır da ekran okuyucunun ne duyduğunu söylüyor: aynı metinden yalnız biri button olarak, biri link olarak duyuluyor, div ise hiçbir şey olarak.
div'i kurtarmak mümkün mü? role="button", tabindex="0" ve Enter ile Space için ayrı bir keydown handler eklersen evet. Ama bu, tarayıcının button ile bedavaya verdiği şeyi üç parça kodla yeniden yazmak demek; bir de disabled durumu, form içindeki davranışı ve yüksek kontrast modunu unutursun. ARIA (Accessible Rich Internet Applications) ile bunun nasıl yapıldığı Accessibility (a11y) serisinin konusu; buradaki cevap kısa: button yaz.
Angular'da da kural aynı. Sayfa değiştiren her şey routerLink'li bir a olmalı; Angular href'i kendisi üretir, böylece yeni sekmede açmak ve link adresini kopyalamak çalışır. (click) handler'ı olan bir div ya da span gördüğünde, angular-eslint'in click-events-have-key-events ve interactive-supports-focus template kuralları bunu yakalar; bu kuralları CI'a (Continuous Integration) almak ilerideki Statik Analiz serisinin konusu. Nu Html Checker bu iki örneği de yakalamıyor: div onclick ve href="#" spec açısından geçerli, sadece yanlış. Checker'ın yakaladığı tek varyant link ile button'ı iç içe koymak:
:4.17-4.38: error: The element “button” must not appear as a descendant of the “a” element.
target="_blank" ve Implicit noopener
Bookstore'da "Önizleme" linki PDF (Portable Document Format) önizlemesini yeni sekmede açıyor: <a href="/preview" target="_blank">. Eski bir uyarı, böyle linklere mutlaka rel="noopener" eklemeyi söyler; çünkü yeni sekmedeki sayfa window.opener üzerinden açan sayfaya ulaşıp onu başka bir adrese yönlendirebiliyordu. Bugün tarayıcılar target="_blank"'e varsayılan olarak noopener davranışı uyguluyor. Bunu iki motorda ölçtüm; ikinci link rel="opener" ile bu davranışı bilerek kapatıyor:
chromium plain: window.opener is null
chromium opener: window.opener is set
webkit plain: window.opener is null
webkit opener: window.opener is set
Yani rel="noopener" artık zorunlu değil; yazmak zararsız. rel="noreferrer" ise başka bir şey yapar: hedef sayfaya Referer header'ını göndermez. Hangisini ne zaman isteyeceğin ve üçüncü parti linklerin güvenlik modeli Frontend Güvenliği serisinin "Üçüncü Parti Script'ler, iframe'ler ve postMessage" yazısında. UX (User Experience) tarafında da bir not: yeni sekmede açılan link kullanıcının geri tuşunu bozar; yalnız gerçekten gerektiğinde, örneğin form doldururken açılan bir yardım sayfasında kullan.
Listeler, Heading'ler ve br
Navigasyon menüsü bir link listesidir ve nav içinde ul olarak yazılır; ekran okuyucu bunun bir liste olduğunu ve kaç link içerdiğini söyleyerek kullanıcıya menünün uzunluğunu bildirir. Sıranın anlam taşıdığı her yerde ol kullan: checkout adımları, kurulum talimatları, "Bu yazarın en çok satan 5 kitabı". ol'un start ve reversed attribute'ları numaralandırmayı CSS'e dokunmadan değiştirir.
Heading'i görsel boyut için seçmek bu ailenin en eski hatası. Kitap kartındaki "Bestseller" etiketini büyük görünsün diye h4 yapmak, ekran okuyucunun heading listesine anlamsız bir satır ekler ve önceki yazıda kurduğumuz seviye düzenini bozar. Heading seviyesi yapıyı, CSS görünümü belirler.
br yalnız içeriğin parçası olan satır sonları içindir: bir adresin satırları, bir şiirin dizeleri. İki paragraf arasında boşluk bırakmak için <br><br> yazmak, ekran okuyucuya tek ve uzun bir paragraf verir; boşluk CSS'in işidir, paragraf p'nin. Konu değişimini göstermek için de bir hr var; adı yatay çizgi olsa da anlamı "tematik geçiş".
Üretimde Dikkat
Metin semantiğinin en büyük düşmanı zengin metin editörleri ve CMS'ler (Content Management System). Bookstore'da kitap açıklamaları yayıncıdan HTML olarak geliyorsa içinde <b>, <br><br>, <font> ve inline style dolu paragraflar bulursun. Bu içeriği sayfaya basmadan önce izin verilen element listesiyle temizlemek hem güvenlik hem semantik için şart; Nu Html Checker'ı bu içeriğin örnekleri üzerinde de çalıştır.
Link ile button kararını tasarım sisteminin seviyesinde ver. Bookstore'un UI (User Interface) kütüphanesinde "buton gibi görünen link" ve "link gibi görünen button" ayrı component'ler olsun ve component'in adı element'i değil, davranışı anlatsın. Tasarımcı "bu bir buton" dediğinde sorulacak soru "tıklayınca nereye gidiyor?" sorusu; cevap bir adresse element a'dır, nasıl göründüğünden bağımsız.
Ölçmek için klavyeyle bir tur at: fareyi bırak, sayfayı yalnız Tab, Shift+Tab, Enter ve Space ile kullanmayı dene. Yukarıdaki script'in yaptığını elle yapmak beş dakika sürer ve tasarım incelemelerinde gözden kaçan div button'ları anında ortaya çıkarır.
Sık Düşülen Tuzaklar
Birincisi, <a href="#"> ya da <a href="javascript:void(0)"> ile aksiyon yazmak. Space ile çalışmaz, adresi ve geçmişi kirletir, ekran okuyucuya yanlış role verir. Aksiyonsa button.
İkincisi, tıklanabilir div ve span. Klavyeyle erişilemez ve Nu Html Checker bunu yakalamaz; yakalayacak olan lint kuralları ve klavye testi.
Üçüncüsü, strong'u ve heading'leri görünüm için kullanmak. Kalınlık ve boyut CSS'tir; element anlam taşır.
Dördüncüsü, time'ın datetime'ına kültüre bağlı bir tarih yazmak. Makinenin okuyacağı değer her zaman ISO 8601.
Beşincisi, abbr title'ı açıklamanın tek yeri yapmak. Dokunmatik ekranda ve çoğu ekran okuyucuda görünmez; kısaltmayı ilk geçtiği yerde metinde aç.
Küçük Bir Deneme
★☆☆ Yazıdaki kitap sayfasını kaydet, Chrome'da aç ve DevTools'ta Elements panelinin Accessibility sekmesinde dl, time ve abbr element'lerinin role'lerine tek tek bak. Sonra künyeyi p ve b ile yeniden yaz ve farkı karşılaştır.
★★☆ actions.html'deki div'i, role="button", tabindex="0" ve bir keydown handler ekleyerek klavyeyle çalışır hâle getir. keyboard.js'i tekrar çalıştır ve clicks listesinin button'unkiyle aynı olduğunu doğrula. Kaç satır yazdığını, sonra o satırları silip yerine tek bir button koymanın kaç satır tuttuğunu say.
★★★ Kendi projende (click) ya da onclick taşıyan bütün div, span ve href="#"'li a element'lerini bul (bir grep yeterli). Her birini "nereye gidiyor / ne yapıyor" sorusuna göre a ya da button olarak sınıfla ve en çok kullanılan üç tanesini düzelt.
Bir sonraki yazıda metinden form'a geçiyoruz: Bookstore'un checkout form'u. Doğru input type, label, fieldset, autocomplete ve inputmode ile mobil klavyenin doğru açılması, ve button'ın varsayılan type'ının neden submit olduğu.