Form Tasarımı: Label Yerleşimi, Hata Mesajları, Inline Validation ve Uzun Form'ları Bölmek
İyi bir form az alan sorar, label'ı alanın üstünde görünür tutar, kullanıcı yazmayı bitirmeden hata göstermez ve hatada ne olduğunu ve nasıl düzeleceğini söyleyip focus'u özete taşır. Bookstore'un teslimat formunu 16 alandan 6'ya, yazılacak 150 karakterden 54'e indiriyor; floating label, yıldız, her tuşta doğrulama, "Invalid" mesajı ve paste engelini ölçüp düzeltiyor, WCAG 2.2'nin Redundant Entry ve Accessible Authentication kriterlerini forma uyguluyoruz.
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 checkout'unda, ödeme adımından önce bir teslimat formu var. Form iki kolonlu, label'lar alanın içinde duruyor ve odaklanınca yukarı kayıyor (floating label), zorunlu alanlar yıldızlı. Doğrulama kodu şu:
const form = document.querySelector('#checkout');
const rules = {
email: (v) => /^\S+@\S+\.\S+$/.test(v),
postcode: (v) => /^\d{5}$/.test(v),
};
// her tuş vuruşunda doğrular
form.addEventListener('input', (event) => {
const input = event.target;
const rule = rules[input.name];
if (!rule) return;
showError(input, rule(input.value) ? '' : 'Invalid');
});
form.addEventListener('submit', (event) => {
event.preventDefault();
for (const input of form.querySelectorAll('input')) {
const required = input.labels[0].textContent.endsWith('*');
if (required && !input.value) showError(input, 'Invalid');
}
});
Ayrıca şifre alanlarında onpaste="return false" var ve formun başlığı "Create an account to continue". Ada e-posta adresini yazmaya başlayıp "ada@" yazdığında ne görür? Hiçbir alanı doldurmadan Continue'ya basan bir kullanıcı kaç hata mesajı görür, bu mesajlar ne der ve klavye focus'u nerede kalır? Şifresini password manager'dan yapıştırmak isteyen biri ne yapar?
Bir önceki yazıya kadar ekranların görsel tarafını kurduk: hiyerarşi, typography, renk, spacing. Bu yazıda kullanıcının en çok emek harcadığı yere, form'a geçiyoruz. HTML tarafı (input type'ları, label, autocomplete, native validation) "HTML5 ve Semantic HTML" serisinin iki form yazısında anlatıldı; burada o element'leri hangi tasarım kararıyla kullandığımıza bakıyoruz.
Önce ölçelim
Script aynı müşteriyi iki forma da sokuyor: görünür ve zorunlu alanları sayıyor, zorunlu alanları doldurmak için kaç karakter yazmak gerektiğini hesaplıyor, e-posta alanına "ada@" yazıp hem yazarken hem alandan çıkınca görünen mesajı okuyor, boş formu gönderip mesajları ve focus'u kaydediyor, son olarak şifre alanına bir paste event'i gönderip engellenip engellenmediğine bakıyor:
node scripts/06-forms.mjs
--- before: 16 visible fields, 15 required, 150 characters to type, 0 with autocomplete
typing "ada@": error while typing "Invalid", after leaving the field "Invalid"
empty submit: 14 messages ("Invalid"), focus on button, title "Checkout - Bookstore"
paste into password blocked: true
--- after: 6 visible fields, 5 required, 54 characters to type, 6 with autocomplete
typing "ada@": error while typing "(none)", after leaving the field "Email address must look like name@example.com"
empty submit: 5 messages ("Enter your email address", "Enter your full name", "Enter your street address", "Enter your city", "Enter your postcode"), focus on summary, title "Error: Checkout - Bookstore"
paste into password blocked: false
Tahminlerin cevabı before satırlarında: Ada daha "ada@" yazarken "Invalid" görüyor, yani yazmayı bitirmeden suçlanıyor. Boş gönderimde 14 kez aynı kelime çıkıyor: "Invalid". Hangi alanın neden geçersiz olduğu, nasıl düzeltileceği yazmıyor; focus da butonda kalıyor, ekran okuyucu kullanıcısı bir şey olduğunu bile duymuyor. Şifreyi yapıştırmak engelli. Bunların her birinin bir tasarım karşılığı var.
Label'ın Yeri: Üstte ve Görünür
Label alanın ne istediğini söyleyen tek şey; kullanıcı yazarken de, formu gönderip kontrol ederken de, hata alıp düzeltirken de görünür kalmalı. kullanılabilirlik araştırma şirketi Nielsen Norman Group'un form önerileri, özellikle mobilde ve kısa formlarda label'ı alanın hemen üstüne koymayı ve label'ı iki alana eşit uzaklıkta bırakmamayı söylüyor. Üstteki label dar ekranda yatay yer harcamıyor, göz label'dan alana tek bir dikey hareketle iniyor ve uzun bir label için yer sorunu çıkmıyor.
Placeholder'ı label yerine kullanmak ise en bilinen hata. Nielsen Norman Group'un placeholder yazısındaki gözlemlere göre kullanıcı yazmaya başlayınca ipucu kayboluyor, form gönderilmeden önce girilenleri kontrol etmek zorlaşıyor, dolu alanlarla boş alanlar ayırt edilemiyor ve soluk gri metin kontrast eşiğinin altında kalıyor.
Floating label neden çözüm değil?
Floating label, label'ı placeholder gibi alanın içinde başlatıp odaklanınca ya da alan dolunca yukarı küçülterek taşıyor. Placeholder'ın "label kayboluyor" sorununu çözüyor, ama başka sorunlar getiriyor. Before'daki CSS'te label dolu alanda 11 piksele küçülüyor; okunması zor ve kontrastı düşük bir etiket oluyor. Label alanın içinde başladığı için yardım metnine yer kalmıyor. Boş bir alanda alanın içinde duran label, bir değer girilmiş gibi görünebiliyor; Nielsen Norman Group da bu yüzden floating label'ı placeholder'dan iyi, alanın dışındaki label'dan kötü buluyor. Yer kazancı da sanıldığı kadar büyük değil: before'da her alan zaten 18 piksellik üst padding ile label'a yer ayırıyor.
Tek kolon
Before'un iki kolonu göz için bir zikzak yaratıyor: Ad, Soyad, sonra E-posta, sonra yan sütunda E-posta tekrar. Kullanıcı hangi sırayla ilerleyeceğini her satırda yeniden çözüyor ve sağ kolondaki bir alanı atlamak kolaylaşıyor. Tek kolonda sıra tek ve belli. İstisna, birlikte okunan kısa alanlar: posta kodu ile şehir ya da son kullanma tarihinin ay ve yılı aynı satırda durabilir. After'da posta kodu alanı bu yüzden dar (max-width: 8rem): alanın genişliği de beklenen cevabın uzunluğu hakkında bir ipucu.
Zorunlu ve Opsiyonel Alanlar
Before'da 16 alandan 15'i zorunlu ve hepsinin label'ında yıldız var. Formun neredeyse tamamı zorunluysa yıldız bir bilgi taşımıyor, yalnızca ekranı kalabalıklaştırıyor. Daha iyi strateji iki adımlı: önce opsiyonel alanları azalt, sonra kalan az sayıdaki opsiyonel alanı açıkça işaretle. GOV.UK (İngiltere hükümetinin dijital hizmetleri; Design System'i açık lisanslı) Design System de opsiyonel alanların label'ına "(optional)" eklemeyi, zorunlu alanları yıldızla işaretlememeyi öneriyor. After'da tek opsiyonel alan telefon numarası; label'ı "Phone number (optional)".
Yardım metni label'ın hemen altında ve alanın üstünde duruyor: "We send the order confirmation here", "Only the courier uses it, on delivery day". Neden istendiğini söylemek, özellikle telefon gibi kişisel bilgilerde kullanıcının tereddüdünü azaltıyor. Yardım metni alanla aria-describedby üzerinden bağlı; ekran okuyucu alana gelince label'dan sonra onu da okuyor.
Alan Sayısını Azaltmak
En iyi alan, hiç sorulmayan alan. Baymard Institute'un 2024 tarihli checkout çalışmasına göre incelenen sitelerde ortalama alan sayısı 11.3 ve çoğu sitenin bütün checkout için 8 alana ihtiyacı var. Aynı kurumun sepet terk etme listesinde katılımcıların yüzde 18'i sitenin hesap oluşturmaya zorlamasını, yüzde 17'si checkout'un uzun ya da karmaşık olmasını sebep olarak gösteriyor.
Before'dan after'a giden yolda alanlar şöyle azaldı:
- Hesap oluşturma zorunlu olmaktan çıktı: başlık "You are checking out as a guest" oldu, şifre alanları ancak kullanıcı "Save my details for next time" seçerse açılıyor. Hesap fikri sipariş tamamlandıktan sonra da sunulabilir.
- "Confirm email" ve "Confirm password" kaldırıldı. E-posta tekrarı yazım hatasını yakalamaz, kullanıcı ilk alanı ikincisine kopyalar; asıl çare onay e-postası ve ileride adres düzeltme imkânı.
- Ad ve soyad tek bir "Full name" alanı oldu. İsimleri iki parçaya bölmek her kültürde işe yaramaz ve kargo etiketi için tek alan yeterli.
- "Address line 2" ve ülke seçimi kaldırıldı: bu örnekte Bookstore yalnız Türkiye içine gönderiyor, ülkeyi sormak gereksiz bir adım.
- Fatura adresi varsayılan olarak teslimat adresiyle aynı; ayrı fatura adresi gereken kullanıcı kutunun işaretini kaldırınca alanlar açılıyor.
Sonuç ölçümde: 16 görünür alan 6'ya, 15 zorunlu alan 5'e, Ada'nın yazması gereken karakter sayısı 150'den 54'e indi. autocomplete değerleri (email, name, street-address, postal-code, tel) sayesinde tarayıcı bu 54 karakterin çoğunu da kendisi doldurabiliyor; WCAG'nin AA (çoğu yasal yükümlülüğün istediği orta seviye) seviyesindeki 1.3.5 Identify Input Purpose kriteri de kullanıcı hakkındaki bilgileri isteyen alanlarda bu amacın programatik olarak belirtilmesini istiyor.
Ne Zaman Doğrulamalı?
Before her tuş vuruşunda doğruluyor. Kullanıcı daha yazmayı bitirmeden "Invalid" görüyor; bu hem yanlış (adres henüz tamamlanmadı) hem de yavaş yazan kullanıcı için yorucu. Nielsen Norman Group'un form hataları üzerine yazısı bu yüzden doğrulamayı kullanıcı alanı terk ettikten sonra yapmayı öneriyor. GOV.UK Design System daha ileri gidiyor ve doğrulamayı yalnız kullanıcı Continue'ya bastığında yapmayı, alandan çıkışta yapmamayı tercih ediyor. İki kaynak farklı bağlamlar için konuşuyor; ortak noktaları erken suçlamamak.
After iki kuralı birleştiriyor. Alandan çıkınca yalnız bir şey yazılmışsa biçim kontrolü yapılıyor; boş alanlar için "zorunlu" hatası ancak submit'te geliyor, böylece Tab ile formda gezinen kullanıcı daha başlamadan bir sürü hata görmüyor. Bir alanda hata gösterildikten sonra ise her tuş vuruşunda yeniden kontrol ediliyor ve değer düzelir düzelmez hata kalkıyor. Kullanıcıyı geç uyar, ama düzelttiğinde hemen ödüllendir:
// geç: kullanıcı alandan çıkınca ve yalnız bir şey yazılmışsa kontrol et
form.addEventListener('focusout', (event) => {
const input = event.target;
if (rules[input.name] && input.value) setError(input, rules[input.name](input.value.trim()));
});
// erken: alan hata gösteriyorsa değer düzelir düzelmez hatayı kaldır
form.addEventListener('input', (event) => {
const input = event.target;
if (input.getAttribute('aria-invalid') === 'true') setError(input, rules[input.name](input.value.trim()));
});
Doğrulamanın tamamı istemcide değil. Posta kodunun gerçekten var olup olmadığını ya da stoktaki son kitabın hâlâ satılabilir olduğunu sunucu bilir; tarayıcıdaki kontrol yalnız kullanıcıyı hızlıca yönlendirmek için. Sunucu tarafındaki doğrulama ve hata yanıtları ASP.NET Core Web API serisinin konusu.
Hata Mesajı, Özet ve Focus
Mesajın kendisi
İyi bir hata mesajı iki şey söyler: ne oldu ve nasıl düzelir. Suçlamaz, teknik dil kullanmaz, label'ın kelimelerini tekrar eder ki kullanıcı mesajı alanla eşleştirsin. GOV.UK'nin rehberi de boş alan için bir talimat ("Enter your…"), yanlış biçim için bir açıklama kullanmayı ve kullanıcının girdiği değeri silmemeyi öneriyor. After'daki mesajlar bu kalıpta:
// ne oldu ve nasıl düzelir, label'ın kelimeleriyle
const rules = {
email: (v) => (!v ? 'Enter your email address' : /^\S+@\S+\.\S+$/.test(v) ? '' : 'Email address must look like name@example.com'),
fullName: (v) => (v ? '' : 'Enter your full name'),
address: (v) => (v ? '' : 'Enter your street address'),
city: (v) => (v ? '' : 'Enter your city'),
postcode: (v) => (!v ? 'Enter your postcode' : /^\d{5}$/.test(v) ? '' : 'Postcode must be 5 digits, like 34710'),
};
Mesaj alanın hemen üstünde, label ile input arasında duruyor ve kalın, --color-danger-text renginde; input'un kenarlığı da kalınlaşıp aynı renge dönüyor. Rengin yanında metin ve kalınlık olduğu için hata yalnız renkle verilmiyor. Mesaj input'a aria-describedby ile bağlanıyor ve input aria-invalid="true" alıyor.
Özet ve focus
Formun başına bir hata özeti eklemek, kullanıcının neyin düzeltilmesi gerektiğini tek bakışta görmesini sağlıyor. Ama özet tek gösterge olmamalı; Nielsen Norman Group'un belirttiği gibi kullanıcı özeti okuduktan sonra ilgili alanı aramak zorunda kalır. After'da ikisi birlikte: üstte bir özet, her alanın yanında kendi mesajı. Özetteki her madde ilgili alana giden bir link.
summary.hidden = errors.length === 0;
document.title = `${errors.length ? 'Error: ' : ''}Checkout - Bookstore`;
if (errors.length) {
summary.focus();
return;
}
Submit'ten sonra focus özete taşınıyor (özetin tabindex="-1"'i bu yüzden var) ve sayfa başlığı "Error:" ile başlıyor. GOV.UK Design System'in önerdiği bu iki adım, klavye ve ekran okuyucu kullanıcısının bir şeylerin ters gittiğini hemen anlamasını sağlıyor. Ölçümdeki "focus on summary" satırı bunun sonucu; before'da focus butonda kalıyor. Dinamik hata bildirimlerinin ekran okuyucuda nasıl duyurulduğu "Accessibility (a11y)" serisinin live region ve form hataları yazısında ayrıntılı ele alınıyor.

Tek Sayfa mı, Çok Adım mı? WCAG 2.2'nin Form'a Etkisi
Checkout'u tek sayfada toplamak ile adımlara bölmek arasında net bir doğru yok. Tek sayfa, kullanıcının bütün formu ve ne kadar kaldığını görmesini sağlar ve sayfa geçişi beklemez; kısa formlarda bu genellikle daha hızlı. Çok adımlı akış, her adımda az alan gösterir, mobilde daha az kaydırma ister ve her adımda yapılan doğrulamayla hatalar erken yakalanır; ama adım sayısı görünür olmalı ve kullanıcı önceki adıma bilgisini kaybetmeden dönebilmeli. GOV.UK'nin "her sayfada tek soru" yaklaşımı bunun en uç hâli; kamu hizmetlerinde, birçok farklı kullanıcının uzun formları doldurduğu bağlamda geliştirilmiş. Bookstore için seçim: teslimat bilgisi tek ekran, ödeme ayrı ekran, en sonda bütün bilgilerin bir arada gösterildiği bir onay ekranı. İki yaklaşımı tartışmak yerine ölçmek istersen A/B testinin nasıl kurulacağı Product Management'a Giriş serisinin deney yazısında.
WCAG 2.2 bu kararı iki kriterle etkiliyor. A seviyesindeki 3.3.7 Redundant Entry, aynı süreçte daha önce girilmiş bir bilginin yeniden istenmemesini, ya otomatik doldurulmasını ya da seçilebilir olmasını istiyor; W3C'nin (web standartlarını yayımlayan kuruluş) kendi örneği de fatura adresinin teslimat adresiyle aynı olduğunu onaylayan bir kutu. Çok adımlı bir akışta özellikle önemli: üçüncü adımda e-postayı tekrar sormak bu kriteri ihlal ediyor. AA seviyesindeki 3.3.8 Accessible Authentication (Minimum) ise giriş yaparken şifre hatırlamak gibi bir bilişsel testi tek yol olarak bırakmamayı istiyor; password manager ve kopyala-yapıştır gibi bir yardım mekanizması yeterli. Before'daki onpaste="return false" tam olarak bu yardımı engelliyor. After'da yapıştırma serbest, alan autocomplete="new-password" taşıyor ve yardım metni password manager'ı açıkça öneriyor.
Üretimde Dikkat
Form analitiği tasarımın en dürüst geri bildirimi: hangi alanda en çok hata görülüyor, hangi alanda kullanıcı formu bırakıyor, hata mesajı gösterildikten sonra kaç kişi düzeltip devam ediyor. GOV.UK da hata mesajlarının ne sıklıkla görüldüğünü izlemeyi öneriyor. Bu ölçümleri kurmak ilerideki "Analytics ve Ölçüm" serisinin konusu.
Gerçek verilerle test et: uzun adlar, kesme işaretli soyadlar, Türkçe karakterli adresler ("Çiçek Sokak No: 3/1"), boşluklu posta kodları ve yapıştırılmış metnin başındaki ve sonundaki boşluklar. After'daki trim() bu sonuncusu için. İsimlerde ve adreslerde kısıtlayıcı regex yazmak, gerçek müşterinin siparişini engellemenin en kolay yolu.
Formun durumunu koru. Bir hata sonrası sayfa yeniden yüklendiğinde ya da kullanıcı ödeme adımından geri döndüğünde girdiği değerler yerinde olmalı; formu silmek, ilk yazıdaki checkout'ta ölçtüğümüz gibi en pahalı hatalardan biri.
Sık Düşülen Tuzaklar
Birincisi, label yerine placeholder ya da floating label. Label her zaman görünür ve alanın dışında olmalı.
İkincisi, her tuş vuruşunda doğrulamak. Kullanıcı yazmayı bitirmeden hata göstermek hem yanlış hem yorucu.
Üçüncüsü, "Invalid" gibi genel mesajlar. Mesaj neyin yanlış olduğunu ve nasıl düzeleceğini, alanın kendi kelimeleriyle söylemeli.
Dördüncüsü, hata sonrası focus'u yerinde bırakmak. Kullanıcı, özellikle klavye ve ekran okuyucu kullanıcısı, hatayı ancak focus ona taşındığında fark eder.
Beşincisi, yapıştırmayı engellemek ve bilgiyi iki kez istemek. İkisi de güvenlik ya da doğruluk sağlamaz, yalnız kullanıcıyı yorar ve WCAG 2.2'yi ihlal eder.
Küçük Bir Deneme
★☆☆ After formunda "Billing address is the same as delivery address" kutusunun işaretini kaldırınca fatura alanlarını gösteren kodu yaz. Alan sayısının ve yazılacak karakter sayısının ne olacağını önce tahmin et, sonra 06-forms.mjs'i bu durum için çalıştır.
★★☆ Script'e bir adım ekle: e-postaya "ada@example" yaz, Tab'a bas, hatayı oku; sonra ".com" ekle ve hatanın her tuş vuruşunda yeniden kontrol edilip kalktığını doğrula.
★★★ Teslimat formunu iki adıma böl (iletişim, adres) ve adımlar arasında geri dönüldüğünde değerlerin korunduğunu, ikinci adımda e-postanın tekrar sorulmadığını ve her adımda hata özetine focus taşındığını script'le doğrula.
Bir sonraki yazıda form'dan bütün interaktif bileşenlere genişliyoruz: affordance ve feedback, her bileşenin default, hover, focus-visible, active, disabled, loading ve error state'leri, skeleton ile spinner arasındaki seçim, optimistic UI ve geri alma, ve Bookstore'un boş sepet, sonuç yok ve API hatası ekranları.