Interaction Design: Affordance, Feedback, Component State'leri, Empty/Loading/Error Ekranları
İyi bir interaktif bileşen her state'ini (hover, focus-visible, active, disabled, loading, error) ayrı tasarlar, 400 ms içinde feedback verir ve boş, sonuç yok ve hata ekranlarında kullanıcıya bir yol gösterir. Bookstore'un sepetinde 803 ms'lik sessiz bekleyişi, görünmeyen focus'u, nedeni söylenmeyen disabled butonu ve "Are you sure?" dialog'unu ölçüp optimistic UI, undo, skeleton ve token'lı focus ring ile düzeltiyoruz.
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. WCAG (Web Content Accessibility Guidelines; web erişilebilirlik standardı) kriterleri 2.2 sürümüne göredir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için bookstore-web'in sepet sayfası şöyle çalışıyor: sayfa açılınca sepet API'den geliyor, her satırda adet için küçük "−" ve "+" butonları ve bir "Remove" linki var, altta teslimat seçimi ve Checkout butonu duruyor. Tasarımcı "outline çirkin görünüyor" dediği için CSS'in başında :focus { outline: none; } var. Davranışın özü şu kod:
cartBox.addEventListener('click', async (event) => {
const row = event.target.closest('tr');
if (event.target.dataset.step) {
const quantity = Number(row.querySelector('.count').textContent) + Number(event.target.dataset.step);
render(await api.setQuantity(row.dataset.id, quantity)); // ekran sunucu cevap verince değişir
}
if (event.target.classList.contains('remove') && confirm('Are you sure?')) {
render(await api.removeItem(row.dataset.id));
}
});
cartBox.addEventListener('change', (event) => {
if (event.target.id === 'delivery') cartBox.querySelector('.checkout').disabled = !event.target.value;
});
Sahte API her çağrıya 800 milisaniye bekleyerek cevap veriyor; sepetin kendisi 1.2 saniyede geliyor. Ada "+"'ya bastığında ekranda ne zaman bir şey değişir? Klavyeyle gezen biri hangi butonda olduğunu görebilir mi? Teslimat seçmeden Checkout'a basmak isteyen kullanıcı butonun neden çalışmadığını nereden öğrenir? Sepet boşsa, API düşmüşse ya da "dnue" diye arama yapılmışsa ekranda ne kalır?
Önce ölçelim
Script iki sayfada aynı adımları yapıyor: 600. milisaniyedeki ekrana ve veri gelince layout'un ne kadar kaydığına (CLS: Cumulative Layout Shift, sayfa yüklenirken içeriğin beklenmedik kayma miktarı) bakıyor, Tab ile her durakta element'in çevresini focus'lu ve focus'suz piksel piksel karşılaştırıyor, hover'da stilin değişip değişmediğine bakıyor, "+"'dan sonra adedin kaç milisaniyede değiştiğini ölçüyor, bir kitabı siliyor ve boş sepet, API hatası, sonuçsuz arama ve stok yetmediği için reddedilen adet artışını deniyor:
node scripts/07-states.mjs
--- before
loading: at 600 ms spinner over the whole page; layout shift (CLS) 0.048
focus: 9 tab stops, visible indicator on 0, a 3:1 change covering a 2 px perimeter on 0
hover: style changes on 0 of 8 buttons and links
"+" button 20x20 px; checkout disabled, no reason shown
"+" clicked: quantity on screen changes after 803 ms
remove: dialogs "Are you sure?"; no undo
empty cart: "Total: ₺0.00", 2 way(s) forward: delivery select | Checkout
API error: alert "Error: 503 Service Unavailable", "(empty)", 0 way(s) forward
search "dnue": "0 results", 0 way(s) forward
"+" when stock runs out: quantity 1, message (nothing), uncaught: 409 Conflict: not enough stock
--- after
loading: at 600 ms 2 skeleton rows in the cart's place; layout shift (CLS) 0.000
focus: 10 tab stops, visible indicator on 10, a 3:1 change covering a 2 px perimeter on 10
hover: style changes on 7 of 8 buttons and links
"+" button 32x32 px; checkout enabled
"+" clicked: quantity on screen changes after 1 ms
remove: dialogs (none); toast "Emma removed from your cart.", Undo brings back 1 row
empty cart: "Your cart is empty", 2 way(s) forward: Browse the classics | Search for a book
API error: "We couldn't load your cart", 2 way(s) forward: Try again | Keep browsing
search "dnue": "No books match "dnue"", 2 way(s) forward: Search for "Dune" | Browse the classics
"+" when stock runs out: quantity 1, message Only 1 in stock right now. We kept your quantity at 1.
Tahminlerin cevabı before satırlarında. "+"'ya basınca ekran 803 milisaniye boyunca hiçbir şey göstermiyor. Dokuz Tab durağının hiçbirinde focus görünmüyor, sekiz butonun hiçbiri üzerine gelince değişmiyor. Checkout kilitli ama nedenini söyleyen bir metin yok. Boş sepet "Total: ₺0.00" yazan boş bir tablo; API hatası bir alert ve arkasında boş bir sayfa; arama "0 results" deyip bırakıyor. Stok yetmediğinde sunucunun cevabı yalnız konsola düşüyor, kullanıcıya hiçbir şey söylenmiyor.
Affordance ve Feedback
Affordance, bir nesnenin nasıl kullanılabileceğini görünüşüyle anlatması. Kavramı tasarım dünyasına Don Norman 1988'de algılanan affordance olarak taşıdı; 2008'deki bir yazısında, kullanıcıya ne yapabileceğini gösteren görünür ipucu için signifier kelimesini önerdi. Ekranda bu ipuçları tanıdık şekiller: dolgu rengi ve kenarlığı olan bir kutu "basılabilir" der, altı çizili mavi metin "beni başka yere götürür" der, kenarlıklı boş bir dikdörtgen "buraya yazılır" der. Before'daki "Remove" küçük mavi bir metin, bir link gibi duruyor ama bir sayfaya gitmiyor, satırı siliyor; görünüşü ile davranışı birbirini tutmuyor.
Feedback ise bileşenin "seni duydum" demesi. Kullanıcı bir şey yaptığında ekran hemen, yaptığı şeyin yakınında ve anlaşılır biçimde değişmeli. İlk yazıdaki "sistem ne durumda, kullanıcı biliyor mu?" sorusu burada bileşen seviyesine iniyor.
Her Bileşenin Bir State Seti Var
Bir butonu tasarlamak, tek bir görünüşünü değil bütün state'lerini tasarlamak demek. Pratikte bir interaktif bileşenin şu state'leri olur ve her biri kendi görünüşünü ister:
| State | Ne zaman | Bookstore sepetinde görünüşü |
|---|---|---|
| default | dokunulmamış hâli | dolgu rengi ya da kenarlık, okunur etiket |
| hover | fare üzerinde | zemin bir ton koyulaşır |
| focus-visible | klavye ile gelinmiş | 2 px mavi outline, 2 px boşlukla |
| active | basılı tutuluyor | 1 px aşağı iner |
| disabled | şu an kullanılamaz | soluk metin, kenarlık açık, fare imleci yasak işareti |
| loading | işlem sürüyor | skeleton ya da "Searching…" metni |
| error | işlem başarısız | satırın altında ne olduğunu söyleyen metin |
| selected | seçili seçenek | işaretli radio |
After'daki butonların CSS'i bu listenin ilk beşini tek yerde topluyor. Focus ring'in rengi ve kalınlığı da artık bir token:
/* 07: focus ring ve hareket */
:root {
--color-focus: var(--blue-600);
--focus-ring: 2px solid var(--color-focus);
--focus-offset: 2px;
--duration-fast: 120ms; /* hover, basma */
--duration-base: 200ms; /* toast, panel */
--ease-out: cubic-bezier(0.2, 0, 0, 1);
}
.btn:hover { background: var(--color-action-hover); }
.btn:active { transform: translateY(1px); }
/* opacity değil: focus ring'i de soluklaştırır */
.btn[aria-disabled="true"], .btn[aria-disabled="true"]:hover { background: transparent; color: var(--color-text-muted); border-color: var(--color-border); cursor: not-allowed; }
:is(.btn, input, a):focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
Focus ring için :focus yerine :focus-visible kullanmak, tasarımcının "outline çirkin" itirazının asıl çözümü. MDN'e (Mozilla'nın web dokümantasyonu) göre tarayıcı focus'un ne zaman görünür olacağına kendi heuristic'iyle karar veriyor: klavyeyle gelindiğinde ring çıkıyor, fareyle tıklanan bir butonda çıkmıyor, metin alanında ise kullanıcının dikkatine ihtiyaç devam ettiği için her durumda çıkıyor. Baseline'a göre 2022'den beri bütün büyük tarayıcılarda var. Yani ring'i kaldırmak yerine yalnız gerektiğinde göstermek mümkün.
Ölçümün "a 3:1 change covering a 2 px perimeter" sütunu WCAG 2.2'nin AAA (en yüksek uygunluk seviyesi) seviyesindeki 2.4.13 Focus Appearance kriterinin ölçüsünü kullanıyor: focus göstergesi en az bileşenin çevresindeki 2 CSS piksel kalınlığındaki bir çerçeve kadar alan kaplamalı ve o piksellerde focus'lu ve focus'suz hâl arasında en az 3:1 kontrast olmalı. AA (çoğu yasal yükümlülüğün istediği orta seviye) seviyesinde zorunlu olan daha basit: 2.4.7 Focus Visible göstergenin görünmesini, 2.4.11 Focus Not Obscured (Minimum) ise focus'lu bileşenin sticky header ya da cookie banner gibi bir şeyin altında tamamen kaybolmamasını istiyor. AAA ölçüsünü hedeflemek, AA'yı da rahatça geçmenin en kolay yolu. Kriterlerin tam ve test edilebilir anlatımı "Accessibility (a11y)" serisinde.
Script ilk denemede bir sürpriz gösterdi: after'da 10 duraktan biri, adedi 1 olan satırdaki "−" butonu, 3:1 ölçüsünü geçemedi. Sebep disabled görünüşü opacity: 0.6 ile vermekti; opacity element'in outline'ını da soluklaştırıyor, ring'in kontrastı düşüyordu. Disabled hâli renklerle verince 10 duraktan 10'u geçti. Hover satırındaki "7 of 8" de bilinçli: disabled butonun üzerine gelince hiçbir şey değişmiyor, değişmemeli.
Disabled Buton Yerine Ne Yapmalı?
Before'daki Checkout butonu teslimat seçilene kadar disabled. Bunun üç sorunu var. Kullanıcı neden basamadığını bilmiyor. Native disabled buton Tab sırasından çıkıyor, ölçümde Checkout'un dokuz durak arasında olmaması bundan; klavye ve ekran okuyucu kullanıcısı butonun varlığını bile fark etmeyebilir.
En iyi çözüm, butonu kilitleyen koşulu ortadan kaldırmak. Beş seçenekli bir select yerine iki seçenekli bir radio grubu ve önceden seçili "Standard, free, 2–3 working days" ile kullanıcıdan karar istemiyoruz; çoğu kullanıcı zaten ücretsiz teslimatı seçiyor. Bu, Hick yasasının pratik karşılığı: William Edmund Hick ve Ray Hyman'ın 1952'deki çalışmalarına dayanan bu kurala göre seçenek sayısı ve karmaşıklığı arttıkça karar süresi uzuyor. Seçenekleri azaltmak ve önerilen seçeneği öne çıkarmak kararı hızlandırıyor.
Koşul kaldırılamıyorsa iki yol var. Butonu aktif bırakıp basıldığında neyin eksik olduğunu söylemek; bir önceki yazıdaki hata özeti bunun örneği. Ya da butonu aria-disabled="true" ile işaretleyip focus alabilir ve yanında nedenini söyleyen bir metin göstermek. After'da adedi 1 olan satırdaki "−" ikinci yolu kullanıyor: ekran okuyucu butonu kullanılamaz olarak duyuruyor, buton Tab sırasında kalıyor ve click handler aria-disabled değerine bakıp hiçbir şey yapmıyor.
Feedback Zamanlaması: 100 ms, 400 ms, 1 Saniye
Ne kadar hızlı cevap vermek gerekiyor? Jakob Nielsen'in 1993'te Usability Engineering kitabında anlattığı üç sınır hâlâ kullanılıyor: 0.1 saniyenin altındaki cevap anlık hissettiriyor ve sonucun kendisi yeterli feedback; 1 saniyeye kadar kullanıcının düşüncesi bölünmüyor ama gecikmeyi fark ediyor; 10 saniyeyi aşınca dikkat başka yere kayıyor ve ne zaman biteceğini gösteren bir ilerleme bilgisi gerekiyor. Laws of UX'in derlediği Doherty eşiği aynı konuya verimlilik tarafından bakıyor: Walter Doherty ve Ahrvind Thadani'nin 1982'de IBM Systems Journal'da yayımladığı çalışmaya göre sistem 400 milisaniyenin altında cevap verdiğinde kullanıcı ile bilgisayar birbirini beklemiyor ve iş hızlanıyor.
Before'un "+" butonu 803 milisaniye boyunca hiçbir şey göstermiyor: Doherty eşiğinin iki katı. After'da adet 1 milisaniyede değişiyor, çünkü ekranı sunucunun cevabını beklemeden güncelliyoruz. Bunun adı optimistic UI ve bir sonraki bölümün konusu.
Skeleton mı, spinner mı?
Sayfanın ilk yüklenmesi için Nielsen Norman Group'un (NN/g; Jakob Nielsen ve Don Norman'ın 1998'de kurduğu kullanılabilirlik araştırma şirketi) skeleton screen yazısı pratik bir ayrım yapıyor: 1 saniyenin altındaki beklemelerde gösterge gerekmiyor, aksine titreme yaratıyor; 10 saniyenin altındaki tam sayfa yüklemelerinde sayfanın yapısını gösteren skeleton, tek bir modülün 2–10 saniyelik beklemesinde spinner, 10 saniyeyi aşan işlerde ilerleme çubuğu uygun. Yalnız header ve footer'ı gösteren "boş çerçeve" tipi skeleton'ı ise önermiyor; içeriğin şeklini taşıyan gri kutular tercih ediliyor.
Before bütün sayfayı yarı saydam bir katmanla kapatıp ortasında spinner döndürüyor. Kullanıcı 1.2 saniye boyunca ne geleceğini bilmiyor, veri gelince tablo ekleniyor ve altındaki "Free delivery" kutusu aşağı itiliyor: CLS 0.048. After'da HTML'in içinde iki satır, teslimat bölümü ve özet şeklinde gri kutular var; kutuların yüksekliği gerçek satırlarla aynı olduğu için veri gelince hiçbir şey kaymıyor, CLS 0.000:
/* skeleton: gerçek satırlarla aynı kutu boyları, veri gelince hiçbir şey zıplamaz */
.skeleton-row { display: grid; grid-template-columns: 1fr 9rem; gap: var(--space-3); padding: var(--space-3) 0; border-bottom: 1px solid var(--color-border); }
.bone { height: 43.5px; border-radius: 4px; background: var(--color-surface); animation: pulse 1.2s ease-in-out infinite; }
.bone.short { height: 32px; align-self: center; }
.delivery-bone { height: 88px; margin-top: var(--space-4); }
.summary-bone { height: 42px; margin-top: var(--space-4); }
@keyframes pulse { 50% { opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) { .bone { animation: none; } }
Skeleton yüksekliklerini tahminle değil ölçerek verdik: ilk denemede bir satır 69, gerçek satır 68.5 pikseldi ve teslimat bölümünün kutusu hiç yoktu; CLS 0.020 çıktı. CLS'in Core Web Vitals içindeki yeri ve ölçümü Web Performansı serisinde.
Optimistic UI ve Undo
Optimistic UI, sunucunun büyük ihtimalle "evet" diyeceği bir işlemde ekranı hemen güncellemek ve nadir "hayır" durumunda geri almak:
// optimistic: ekranı hemen değiştir, sunucu reddederse geri al
async function changeQuantity(id, step) {
const item = items.find((i) => i.id === id);
const previous = item.quantity;
if (previous + step < 1) return;
item.quantity = previous + step;
item.error = '';
render();
try {
await api.setQuantity(id, item.quantity);
} catch {
item.quantity = previous;
item.error = `Only ${previous} in stock right now. We kept your quantity at ${previous}.`;
render();
}
}
Geri alma kısmı optimistic UI'ın asıl işi. Ölçümün son satırı bunu gösteriyor: stok yetmediğinde after adedi 1'e geri çekiyor ve satırın altına ne olduğunu ve neyi koruduğunu söyleyen bir cümle yazıyor; before'da aynı hata yakalanmamış bir exception olarak konsolda kalıyor. Optimistic UI'ı nerede kullanmamak gerektiği de önemli: ödeme, sipariş oluşturma ya da stok rezervasyonu gibi sonucu sunucunun belirlediği ve geri alınması pahalı işlemlerde kullanıcıya "tamamlandı" demeden önce cevabı beklemek gerekiyor. Orada doğru feedback, ilk yazıdaki gibi butonun hemen "Processing payment..." durumuna geçmesi ve ikinci tıklamayı kabul etmemesi.
"Are you sure?" yerine Undo
Before bir kitabı silmeden önce confirm('Are you sure?') soruyor. NN/g'nin onay dialog'ları üzerine 2018 tarihli yazısına göre sık ve genel onay soruları kullanıcıya düşünmeden "OK"e basmayı öğretiyor; dialog bir süre sonra korumayı bırakıyor ve iş akışını bölmeye devam ediyor. Aynı yazı, mümkün olan yerde geri almayı daha iyi bir güvence olarak görüyor ve onayı sonucu ciddi, geri alınamayan işlemlere ayırmayı öneriyor; o zaman bile soru genel değil spesifik olmalı ve butonlar "Yes/No" yerine sonucun adını taşımalı. Sepetten bir kitabı çıkarmak ne ciddi ne geri alınamaz, bu yüzden after silip bir toast ile geri alma imkânı veriyor:
// "Are you sure?" yerine undo
let toastTimer;
async function removeItem(id) {
const index = items.findIndex((i) => i.id === id);
const [removed] = items.splice(index, 1);
render();
showToast(`${removed.title} removed from your cart.`, async () => {
items.splice(index, 0, removed);
render();
await api.restoreItem(removed, index);
});
await api.removeItem(id);
}
Toast'ın da sınırları var. Kendi kendine kaybolan bir mesaj, okuması yavaş ya da klavyeyle oraya ulaşmaya çalışan kullanıcı için kaçırılan bir mesaj olabilir. After'da toast 8 saniye duruyor, fare üzerindeyken ya da focus içindeyken süre işlemiyor ve toast kutusu sayfa açılırken role="status" ile DOM'da (sayfanın tarayıcıdaki element ağacı) hazır bekliyor; böylece içine yazılan mesaj ekran okuyucuda duyuruluyor (WCAG 4.1.3 Status Messages). Toast'a, başka hiçbir yerde bulunmayan bir bilgi ya da işlemin tek yolu konmamalı: sepet satırı Undo'ya basılmasa da sayfanın kendisinden geri eklenebiliyor.
Fitts Yasası, Hedef Boyutu ve Micro-interaction'lar
Paul Fitts'in 1954'te ortaya koyduğu Fitts yasasına göre bir hedefe ulaşma süresi hedef uzaklaştıkça artıyor, hedef büyüdükçe azalıyor. Before'daki adet butonları 20×20 piksel ve birbirine neredeyse yapışık; hızlı bir tıklama yanlış butona gidebiliyor. After'da 32×32 piksel ve aralarında boşluk var. WCAG 2.2'nin 2.5.8 Target Size (Minimum) kriteri 24×24 piksel istiyor, platform rehberleri daha büyük değerler öneriyor; bu farkı ve mobilde başparmak erişimini 09. yazıda ayrıntılı ele alıyoruz. Fitts'in ikinci yarısı da önemli: Checkout butonu toplam tutarın hemen yanında, kullanıcının gözünün ve imlecinin zaten bulunduğu yerde.
Micro-interaction'lar, yani hover'da zeminin koyulaşması, basınca butonun 1 piksel inmesi ve toast'ın kayarak gelmesi, feedback'in en küçük birimleri. Süreleri kısa olmalı ve token'dan gelmeli; 120 ve 200 milisaniyelik iki süre bu ekranın bütün hareketine yetiyor. Hareket bazı kullanıcılar için baş dönmesi ve mide bulantısı yaratabiliyor; işletim sisteminde "hareketi azalt" ayarını açan kullanıcının tercihi prefers-reduced-motion ile okunabiliyor ve token'lar burada da işi kolaylaştırıyor:
@media (prefers-reduced-motion: reduce) {
:root {
--duration-fast: 0ms;
--duration-base: 0ms;
}
}
Bütün geçişler süreyi token'dan aldığı için tek bir media query hepsini kapatıyor; skeleton'ın nabız animasyonu da aynı media query ile duruyor. WCAG'nin AAA seviyesindeki 2.3.3 Animation from Interactions kriteri de etkileşimle tetiklenen hareketin kapatılabilmesini istiyor.
Boş, Sonuç Yok ve Hata Ekranları
Bu üç ekran çoğu projede en son düşünülen, ama kullanıcının en çok yardıma ihtiyaç duyduğu ekranlar. Üçünün tasarım kuralı aynı: ne olduğunu kullanıcının diliyle söyle ve en az bir yol göster. Ölçümün "way(s) forward" sütunu bunu sayıyor: before'un boş sepetinde iki "yol" var ama ikisi de anlamsız (boş bir sepet için teslimat seçmek ve kilitli Checkout), API hatasında ve sonuçsuz aramada sıfır.

Boş sepet aslında bir başlangıç ekranı: kullanıcıya sepetin ne işe yaradığını tek cümleyle söyleyip kataloğa ve aramaya götürüyor. Sonuç yok ekranı aramanın bittiği değil yön değiştirdiği yer. After, aranan kelimeye en fazla iki harf uzaklıktaki kitap adını buluyor ve "Search for "Dune"" butonu sunuyor; yazım önerisi yoksa kataloğa yönlendiriyor.
results.innerHTML = `
<div class="state">
<h2>No books match "${query}"</h2>
<p>Check the spelling or search by author.</p>
<div class="actions">
${close ? `<button class="btn suggestion" data-q="${close.title}">Search for "${close.title}"</button>` : ''}
<a class="btn btn-secondary" href="/web/05-spacing/after.html">Browse the classics</a>
</div>
</div>`;
Hata ekranı, backend tarafında graceful degradation (bir parça çalışmadığında bütün sayfayı düşürmeden azaltılmış hizmet vermek) dediğimiz yaklaşımın kullanıcıya görünen yüzü. API düştüğünde exception fırlatmamak yetmiyor, kullanıcıya ne olduğunu, verisinin güvende olup olmadığını ve ne yapabileceğini söylemek gerekiyor. Before'un alert("Error: 503 Service Unavailable") mesajı bunların hiçbirini yapmıyor: HTTP durum kodu kullanıcı için anlamsız, alert kapandıktan sonra ekranda hiçbir şey kalmıyor. After'daki mesaj "We couldn't load your cart" diyor, kitapların kayıtlı olduğunu söylüyor ve "Try again" ile "Keep browsing" sunuyor; role="alert" sayesinde ekran okuyucu da hemen duyuruyor. Hata mesajlarının yazımı için bir önceki yazıdaki "ne oldu + nasıl düzelir" kuralı burada da geçerli.
Üretimde Dikkat
State'leri tasarım dosyasında değil gerçek bileşende gör. Hover, focus-visible, disabled ve error hâllerini bir tablo gibi yan yana gösteren tek bir test sayfası, eksik state'i ilk bakışta ortaya çıkarıyor; component library'lerdeki "story" mantığı bu. 10. yazıda design system'le birlikte bu fikre döneceğiz.
Gecikmeyi yerelde değil gerçek ağ koşullarında dene: lokal makinede her API 10 milisaniyede cevap verdiği için skeleton ve optimistic UI gereksiz görünüyor. Playwright ve DevTools'taki ağ yavaşlatma bunun için yeterli.
Optimistic UI'ın geri alma yolunu da test et. Sunucu 409 ya da 503 döndüğünde ekranın doğru değere döndüğünü doğrulayan bir senaryo, before'daki "konsola düşen hata"yı üretime çıkmadan yakalıyor. Hataların ve boş ekranların ne sıklıkla görüldüğünü ölçmek ise ilerideki "Analytics ve Ölçüm" serisinin konusu.
Sık Düşülen Tuzaklar
Birincisi, outline: none ile focus'u silmek. Doğrusu :focus-visible ile ring'i yalnız gerektiğinde göstermek ve rengini token'a bağlamak.
İkincisi, disabled görünüşü opacity ile vermek. Opacity focus ring'i de soluklaştırıyor; ölçümde tam olarak bu yüzden bir buton 3:1 eşiğini geçemedi.
Üçüncüsü, nedenini söylemeyen disabled buton. Önce koşulu kaldırmaya çalış; olmuyorsa butonu aktif bırakıp eksik olanı söyle ya da aria-disabled ile focus'ta tut ve nedenini yanına yaz.
Dördüncüsü, her silme işlemine "Are you sure?" sormak. Geri alınabilen işlemlerde undo, ciddi ve geri alınamayan işlemlerde spesifik bir onay.
Beşincisi, boş, sonuç yok ve hata ekranlarını unutmak. Her biri ne olduğunu söyleyen bir cümle ve en az bir yol ister.
Küçük Bir Deneme
★☆☆ After'daki toast'ın süresini 8 saniyeden 3 saniyeye indir ve script'e "toast görünür olduktan sonra Tab ile Undo butonuna ulaşmak kaç saniye sürüyor?" sorusunu ölçen bir adım ekle. Kısa toast'ın klavye kullanıcısı için ne anlama geldiğini sonuçtan yorumla.
★★☆ Checkout linkine bir loading state ekle: tıklanınca "Saving your cart…" yazsın, aria-disabled="true" olsun ve 1.2 saniye sonra 06 numaralı formun after sayfasına gitsin. İkinci tıklamanın ikinci bir istek göndermediğini script'le doğrula.
★★★ Sayfayı colorScheme: 'dark' ile aç ve focus ölçümünü tekrar çalıştır. Toast içindeki Undo butonunun ring'i ters çevrilmiş zeminde de 3:1 eşiğini geçiyor mu? Geçmiyorsa sebebini bul ve düzelt.
Bir sonraki yazıda tek bir ekrandan bütün sitenin yapısına çıkıyoruz: bilgi mimarisi, kategori ağacı, breadcrumb, Bookstore kataloğunda URL'ye yansıyan facet filtreler ve aktif filtre chip'leri, arama önerileri, ve pagination ile "daha fazla yükle" ve infinite scroll arasındaki seçim.