İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

Blok 6 · Ders 45

Stil, erişilebilirlik ve kullanıcı deneyimi

Tailwind CSS ile hızlı stil, duyarlı tasarım, erişilebilirlik kontrolleri ve küçük ama fark yaratan arayüz ayrıntıları.

Bu dersin sonunda

  • Tailwind CSS'in yardımcı sınıf mantığını düz CSS'le karşılaştırıp artısını ve eksisini anlatabilirsin
  • sm:, lg: önekleriyle mobil öncelikli, dark: önekiyle koyu temaya uyan sayfalar yazabilirsin
  • hover:, focus-visible: ve disabled: durumlarıyla düğme, kart ve form bileşenleri kurabilirsin
  • Bir sayfayı etiket, odak, kontrast ve klavye açısından denetleyip ARIA'yı yalnız gerektiğinde kullanabilirsin
  • Lighthouse'u geliştirici araçlarında çalıştırıp puanları ve erişilebilirlik bulgularını okuyabilirsin
  • Boş, yükleniyor (iskelet) ve hata durumlarını kullanıcıya ne olduğunu ve ne yapacağını söyleyen biçimde tasarlayabilirsin
Bu sayfada
  1. Yeni proje: stil atölyesi
  2. Tailwind'in mantığı: yardımcı sınıflar
  3. Önekler: koşula bağlı sınıflar
  4. Düğme: durumları olan bir bileşen
  5. Kart: bir maçın bütün bilgisi
  6. Form: etiket, girdi ve hata mesajı
  7. Erişilebilirlik: dört denetim
  8. Lighthouse ile ölçmek
  9. Boş, yükleniyor ve hata durumları
  10. Görevler
  11. Kendini sına
  12. Kayıt noktası

Uygulaman çalışıyor; ama çalışan bir uygulama ile kullanılan bir uygulama arasında fark var. Kullanıcı bir düğmenin düğme olduğunu anlamalı, telefonda yazıları büyütmeden okuyabilmeli, fareyi kullanamıyorsa klavyeyle her yere ulaşabilmeli, internet yavaşken ekranın donmadığını görmeli. Bu derste Tailwind CSS ile hızlı stil yazmayı, sayfayı telefona ve koyu temaya uydurmayı, düğme, kart ve form bileşenleri kurmayı, sayfanı erişilebilirlik açısından denetleyip Lighthouse ile ölçmeyi ve boş, yükleniyor, hata durumlarını tasarlamayı öğreneceksin. Dersin sonunda bir maç geçmişi sayfan olacak: kartlar, form, iskelet yükleyici, boş ve hata ekranlarıyla. Ders 46'daki projen aynı parçaları kullanacak.

Yeni proje: stil atölyesi

Önceki projelerinle karışmasın diye yeni bir Next.js projesi açıyorsun. Komut Ders 43'tekiyle aynı; yalnız adı farklı. VS Code'da bir terminal aç, cd C:\kod ile kod klasörüne geç ve yaz:

Terminal — PowerShell
PS C:\kod> npx create-next-app@latest stil-atolyesi --ts --app --tailwind --eslint --no-src-dir --import-alias "@/*" --no-react-compiler --no-cache-components --use-npm

Çıktı Ders 43'tekiyle aynı akar; en sonda Success! Created stil-atolyesi at C:\kod\stil-atolyesi yazar. Bu ders create-next-app 16.4.0 ve Tailwind CSS 4.3.3 ile denendi (npm view tailwindcss version dersi yazarken 4.3.3 diyordu).

--tailwind bayrağı Tailwind'i kurdu ve ayarladı; sen bir şey kurmayacaksın. Ne kurduğuna bakmak için package.json'daki devDependencies bölümünde tailwindcss ve @tailwindcss/turbopack satırlarını, next.config.ts'te de @tailwindcss/turbopack yazan turbopack bölümünü görebilirsin: Next'in derleme aracı Turbopack, CSS dosyalarını bu eklentiden geçiriyor.

Klasörü aç (code stil-atolyesi) ve geliştirme sunucusunu başlat (npm run dev, adres http://localhost:3000). Şimdi şablonu temizle. app/globals.css'te yalnız ilk satır kalsın:

app/globals.css
@import "tailwindcss";

app/layout.tsx'in içini tamamen sil ve şunu yaz. Şablonun yazı tipi satırlarını bu derste kullanmıyoruz; sayfalar sistemin yazı tipiyle çizilecek:

app/layout.tsx
import type { Metadata } from "next"import "./globals.css" export const metadata: Metadata = {    title: "Stil Atölyesi",    description: "Tailwind CSS ve erişilebilirlik denemeleri",} export default function RootLayout({ children }: LayoutProps<"/">) {    return (        <html lang="tr">            <body>{children}</body>        </html>    )}

app/page.tsx'e de hiç stil vermeden bir taslak yaz:

app/page.tsx
export default function Home() {    return (        <main>            <h1>Maç geçmişi</h1>            <p>Son maçlarına bak, yenisini ekle.</p>            <button>Maç ekle</button>            <ul>                <li>Ahri · Zafer · 8/2/11</li>                <li>Jinx · Bozgun · 4/7/6</li>            </ul>            <label htmlFor="sampiyon">Şampiyon</label>            <input id="sampiyon" />        </main>    )}
localhost:3000
Beyaz zeminde alt alta altı satır düz yazı: Maç geçmişi, Son maçlarına bak yenisini ekle, Maç ekle, Ahri Zafer 8/2/11, Jinx Bozgun 4/7/6, Şampiyon. Başlık büyük değil, düğmenin kenarı yok, listenin madde işareti yok, giriş kutusu hiç görünmüyor.
Stil yokken sayfa. Düğme, liste ve giriş kutusu orada ama hiçbiri öyle görünmüyor.

Dikkatle bak: "Maç ekle" bir düğme ama düz yazı gibi duruyor. Listenin madde işaretleri yok. "Şampiyon" yazısının sağında bir giriş kutusu var ama görünmüyor; üstüne tıklarsan içine yazabilirsin. Tailwind'in Preflight adlı sıfırlama katmanı tarayıcıların birbirinden farklı varsayılan stillerini siliyor: boşluklar sıfır, başlıklar sıradan yazı boyutunda, listeler işaretsiz, kenarlıklar sıfır kalınlıkta. Her tarayıcıda aynı, boş bir tuvalden başlıyorsun. Bu dersin işi o tuvali boyamak.

Tailwind'in mantığı: yardımcı sınıflar

Ders 43'te globals.css'e 140 satıra yakın CSS koymuştun: .ust, .kart, .harf, .favori… Her yeni görünüm için bir ad uydurup CSS dosyasına geçiyor, kuralı yazıp bileşene dönüyordun. Bu düzenin üç derdi var. Ad bulmak zor (.kart mı, .mac-karti mı, .kart-kucuk mu?). İki dosya arasında gidip gelirsin. Bir bileşeni silince CSS'teki kuralları da silmeyi unutursun; dosya hiç kullanılmayan kurallarla şişer, hangisinin hâlâ lazım olduğunu kimse bilemez.

Tailwind CSS başka bir yol önerir: her biri tek bir iş yapan hazır, küçük sınıflar. p-4 iç boşluk verir, font-bold yazıyı kalınlaştırır, bg-white zemini beyaz yapar. Görünümü, bu sınıfları doğrudan JSX'teki className'e dizerek kurarsın.

İlk sınıflar

app/page.tsx'teki taslağın düğmesini, listesini ve formunu şimdilik sil; her birini bu derste bir bileşen olarak geri getireceksin. Kalan başlığa ve paragrafa sınıf ver:

app/page.tsx
export default function Home() {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <h1 className="text-3xl font-bold">Maç geçmişi</h1>            <p className="mt-1 text-zinc-600">Son maçlarına bak, yenisini ekle.</p>        </main>    )}

layout.tsx'teki <body>'ye de sayfanın zemin ve yazı rengini ver:

app/layout.tsx
            <body className="bg-zinc-50 text-zinc-900">{children}</body>

Kaydet: başlık büyüdü ve kalınlaştı, paragraf griye döndü, içerik ortalanıp kenarlardan boşluk aldı. Her sınıf tek bir CSS satırının kısaltması:

SınıfÜrettiği CSS (değerler açılmış)Anlamı
mx-automargin-inline: autosağdan ve soldan eşit boşluk: ortala
max-w-5xlmax-width: 64remen çok 1024 piksel genişlik
px-4padding-inline: 1remsağa ve sola 16 piksel iç boşluk
py-8padding-block: 2remüste ve alta 32 piksel iç boşluk
mt-1margin-top: 0.25remüstte 4 piksel dış boşluk
text-3xlfont-size: 1.875rem30 piksel yazı (satır yüksekliğiyle birlikte)
font-boldfont-weight: 700kalın
text-zinc-600color: …zinc (çinko grisi) paletinin 600 tonu
bg-zinc-50background-color: …aynı paletin en açık tonu

İki kalıp hemen göze çarpıyor:

  • Boşluk ölçeği: Sınıftaki sayının bir birimi 0.25rem, yani 4 piksel. p-1 4, p-2 8, p-4 16, p-8 32 piksel. p iç boşluk (padding), m dış boşluk (margin); x yatay, y dikey, t üst, b alt, l sol, r sağ demek. mt-6 "üstten 24 piksel dış boşluk"tur. Ders 20'deki kutu modelinin aynısı, yalnız kısaltılmış.
  • Renk paleti: zinc, violet, red, emerald gibi renk adları ve 50'den 950'ye tonlar. 50 neredeyse beyaz, 950 neredeyse siyah. text- yazı rengi, bg- zemin rengi, border- kenar rengi verir.

Hangi sınıfın hangi CSS'e karşılık geldiğini ezberlemene gerek yok. Tarayıcıda başlığa sağ tıkla, İncele (Inspect) de; Styles panelinde sınıfların ürettiği kuralları görürsün. Tailwind sayıları CSS değişkenleriyle (Ders 21) yazar; panelde şöyle bir şey okursun:

Styles panelinde (Tailwind'in ürettiği CSS'ten)
.text-3xl {    font-size: var(--text-3xl);    line-height: var(--tw-leading, var(--text-3xl--line-height));} .px-4 {    padding-inline: calc(var(--spacing) * 4);}

--spacing değişkeninin değeri 0.25rem; calc(var(--spacing) * 4) yani 1rem. Bütün boşluklar aynı ölçekten geldiği için sayfanda 13 piksel, 17 piksel gibi gelişigüzel değerler olmaz; tasarım kendiliğinden düzenli durur.

Tailwind CSS'i nasıl üretir?

Tailwind'de binlerce olası sınıf var; hepsinin CSS'i sitene girse dosya devasa olurdu. Bunun yerine Tailwind projendeki dosyaları düz metin olarak tarar, içlerinde sınıf adına benzeyen sözcükleri toplar ve yalnız onların CSS'ini üretir. Kodunu çalıştırmaz; yalnız okur. Bu ayrıntı birazdan önemli bir hatanın sebebi olacak.

Dugme.tsx "bg-violet-700" "text-white" RenkliDugme.tsx `bg-${renk}-700` renk çalışınca "emerald" olur Tailwind dosyayı metin olarak okur, kodu çalıştırmaz Üretilen CSS .bg-violet-700 ✓ var .text-white ✓ var .bg-emerald-700 ✗ yok: tam adı hiçbir dosyada geçmiyor
Şekil 45.1 Tailwind dosyalarını metin olarak okur. Tam adı yazılı olan sınıfın CSS'i üretilir; parça parça kurulan sınıfın üretilmez.

Tailwind node_modules'u, .gitignore'daki dosyaları, resimleri ve CSS dosyalarını taramaz; geri kalan her şeye bakar. Sitenin CSS'i bu yüzden küçük kalır: bu dersin bütün sayfalarının (görevler dahil) yayın derlemesindeki CSS dosyası yaklaşık 17 KB tuttu.

Artıları, eksileri

Düz CSS (Ders 20–21, 43)Tailwind
Ad bulmakHer görünüme bir sınıf adı uydurursunAd yok; hazır sınıfları dizersin
DosyaJSX ile CSS dosyası arasında gidip gelirsinGörünüm bileşenin içinde
SilmekBileşeni silince ölü CSS kalırBileşenle birlikte stili de gider
TutarlılıkHer değeri sen seçersin (13 px, 17 px…)Ortak ölçek: boşluk, renk, yazı boyutu
OkunurlukJSX sadeclassName uzar, ilk bakışta kalabalık
ÖğrenmekCSS bilmen yeterSınıf adlarını öğrenmen gerekir

Tailwind CSS'in yerine geçmez; CSS bilmeden Tailwind yazamazsın. flex, grid, justify-between, rounded Ders 20–21'de öğrendiğin özelliklerin kısa adları. Uzayan className sorununun çözümü de tanıdık: aynı sınıf listesini iki yerde tekrarlıyorsan onu bir React bileşenine taşırsın. Birazdan düğmeyle yapacağın şey bu.

Sık hataEski kurulumun üç @tailwind satırı

Ne zaman çıkar? Tailwind 3 anlatan bir yazıdan globals.css'in başına şu üç satırı kopyalayınca:

app/globals.css✗ Yanlış
@tailwind base;@tailwind components;@tailwind utilities;

Ne terminalde ne tarayıcıda bir hata mesajı çıkar. Sayfa açılır ama tuhaftır: flex ve grid gibi düzen sınıfları çalışır, renkler, boşluklar ve köşeler gelmez; başlıklar tarayıcının varsayılan büyük, kalın hâline, listeler madde işaretlerine döner. Bu dersin sonunda yazacağın maç kartlarıyla denediğimizde kartın (article) iç boşluğu ve zemini konsolda şöyle çıktı:

Konsol (F12 → Console)
> getComputedStyle(document.querySelector("article")).padding'0px'> getComputedStyle(document.querySelector("article")).backgroundColor'rgba(0, 0, 0, 0)'

p-4 yazdığın kartın iç boşluğu 0, zemini saydam. Tailwind 4 bu satırlardan yalnız utilities'i tanıyor; renkleri ve ölçüleri tanımlayan tema ile Preflight hiç yüklenmiyor. Tema olmayınca Tailwind, değerini temadan alan p-4, bg-white, rounded-xl, font-bold gibi sınıfların CSS'ini hiç üretmiyor; yalnız temaya ihtiyacı olmayan flex ve grid gibi sınıfları üretiyor. Aynı üç satırı Tailwind 4.3.3'ün derleyicisinden geçirdik: çıkan CSS'te .flex ve .grid vardı, .p-4 ve .bg-white yoktu.

Nasıl düzelir? Üç satırı sil, yerine tek satır yaz: @import "tailwindcss";. Değişiklik görünmezse dosyayı bir kez daha kaydet ya da sunucuyu Ctrl+C ile durdurup yeniden başlat.

Sık hatanpm error could not determine executable to run

Ne zaman çıkar? Eski bir yazıdaki npx tailwindcss init -p komutunu Tailwind 4 kurulu bir projede çalıştırınca.

Terminal — PowerShell
PS C:\kod\stil-atolyesi> npx tailwindcss init -pnpm error could not determine executable to runnpm error A complete log of this run can be found in: C:\Users\Deniz\AppData\Local\npm-cache\_logs\2026-10-12T01_49_51_124Z-debug-0.log

"Çalıştırılacak programı bulamadım." Tailwind 4'ün tailwindcss paketinde komut satırı programı yok; ayar dosyası da artık gerekmediği için init komutu kaldırıldı.

Nasıl düzelir? Bir şey yapmana gerek yok: create-next-app --tailwind her şeyi kurdu. Bu komutu öneren yazının Tailwind 3 için yazıldığını anla ve kurulum için resmî belgeye dön.

Önekler: koşula bağlı sınıflar

Ders 20'de :hover yazmıştın: "fare üstündeyken şu kural geçerli". Ders 21'de @media (min-width: 640px): "pencere en az 640 piksel genişse şu kural geçerli". Tailwind ikisini de aynı yolla yazar: sınıfın başına bir önek koyarsın. hover:bg-violet-600 "fare üstündeyken zemini mor yap", sm:grid-cols-2 "640 pikselden itibaren iki sütun", dark:bg-zinc-900 "koyu temadaysa zemini koyu gri yap" demek.

Önekler üst üste de binebilir:

dark:md:hover:bg-violet-600 dark: koyu temadaysa md: pencere en az 768 piksel genişse hover: fare öğenin üstündeyse bg-violet-600 asıl iş: zemini mor yap Üç koşul birden tutarsa sınıf çalışır; biri bile tutmazsa yokmuş gibi.
Şekil 45.2 Bir sınıfın parçaları: önekler koşulları, en sağdaki parça asıl işi söyler.

Duyarlı sınıflar: sm:, md:, lg:

Tailwind de Ders 21'deki gibi mobil öncelikli çalışır. Öneksiz sınıf her genişlikte geçerlidir; önekli sınıf o genişlikten itibaren devreye girer ve öncekini ezer:

ÖnekBaşladığı genişlikÜrettiği medya sorgusu
sm:40rem (640 px)@media (width >= 40rem)
md:48rem (768 px)@media (width >= 48rem)
lg:64rem (1024 px)@media (width >= 64rem)
xl:80rem (1280 px)@media (width >= 80rem)
2xl:96rem (1536 px)@media (width >= 96rem)

Sayfaya maçları ekleyerek dene. Önce bir veri dosyası aç: proje kökünde lib klasörü ve içine maclar.ts:

lib/maclar.ts
export type Mac = {    id: number    sampiyon: string    sonuc: "zafer" | "bozgun"    oldurme: number    olum: number    asist: number    dakika: number} export const maclar: Mac[] = [    { id: 1, sampiyon: "Ahri", sonuc: "zafer", oldurme: 8, olum: 2, asist: 11, dakika: 31 },    { id: 2, sampiyon: "Jinx", sonuc: "bozgun", oldurme: 4, olum: 7, asist: 6, dakika: 27 },    { id: 3, sampiyon: "Garen", sonuc: "zafer", oldurme: 6, olum: 3, asist: 4, dakika: 24 },    { id: 4, sampiyon: "Lux", sonuc: "zafer", oldurme: 3, olum: 1, asist: 17, dakika: 35 },]

Sonra page.tsx'te paragrafın altına bir ızgara koy (en üste import { maclar } from "@/lib/maclar" satırını eklemeyi unutma):

app/page.tsx
            <ul className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">                {maclar.map((mac) => (                    <li key={mac.id} className="rounded-xl bg-white p-4">                        {mac.sampiyon}                    </li>                ))}            </ul>

grid ızgarayı açar ve öneksiz hâlde tek sütundur; sm:grid-cols-2 640 pikselden itibaren iki, lg:grid-cols-3 1024 pikselden itibaren üç sütun yapar. Geliştirici araçlarında Ctrl+Shift+M ile cihaz araç çubuğunu aç ve genişliği değiştir. Sınırların iki yanını ölçtük:

Pencere genişliğiSütun
639 px1
640 px2
1023 px2
1024 px3
Pencere genişliği (piksel) telefon (390) dizüstü (1280) 0 sm 640 md 768 lg 1024 xl 1280 2xl 1536 1 sütun 2 sütun (sm:) 3 sütun (lg:) grid-cols-1 sm:grid-cols-2 lg:grid-cols-3
Şekil 45.3 Önekli sınıf, o genişlikten itibaren geçerlidir. Altındaki şerit grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'ün sonucu.

Koyu tema: dark:

Windows'ta Ayarlar → Kişiselleştirme → Renkler → Modunuzu seçin bölümünde (İngilizce Windows'ta Settings → Personalization → Colors → Choose your mode) koyu modu seçen biri, sitelerin de koyu olmasını bekler. Tarayıcı bu tercihi sayfalara prefers-color-scheme medya özelliğiyle bildirir. Tailwind'in dark: öneki varsayılan olarak tam bunu dinler: kullanıcının sistemi koyuysa dark: önekli sınıflar devreye girer.

layout.tsx'teki iki satırı şöyle değiştir:

app/layout.tsx
        <html lang="tr" className="scheme-light-dark">            <body className="bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">{children}</body>

page.tsx'te de paragrafa ve liste ögelerine koyu karşılıklarını ekle:

TSX
<p className="mt-1 text-zinc-600 dark:text-zinc-400">Son maçlarına bak, yenisini ekle.</p>
TSX
<li key={mac.id} className="rounded-xl bg-white p-4 dark:bg-zinc-900">
  • dark:bg-zinc-950 koyu temada zemini neredeyse siyah, dark:text-zinc-100 yazıyı neredeyse beyaz yapar. Açık temada bu sınıflar yokmuş gibidir.
  • scheme-light-dark, Ders 25'teki color-scheme özelliğinin Tailwind karşılığı: tarayıcıya "bu sayfa iki temayı da destekliyor" der. Kaydırma çubuklarını, açılır listeleri ve onay kutularını tarayıcı da temaya uygun çizer.

Windows'un temasını değiştirmeden denemek için geliştirici araçlarında Ctrl+Shift+P ile komut menüsünü aç, rendering yaz ve Show Rendering'i seç. Açılan Rendering sekmesinde Emulate CSS media feature prefers-color-scheme listesinden prefers-color-scheme: dark'ı seç (arayüz değişmiş olabilir). Sayfa anında koyulaşır.

Kural basit: renk veren her sınıfın bir koyu karşılığı olmalı. bg-white yazıp dark:bg-… yazmayı unutursan koyu temada beyaz bir kart parlar; daha kötüsü, body'den gelen açık renkli yazı beyaz kartın üstünde görünmez olur.

Düğme: durumları olan bir bileşen

Taslaktaki düğme düz yazı gibi duruyordu. Bir düğme kullanıcıya üç şey söylemeli: "bana basılabilir" (biçim ve imleç), "şu an üstümdesin" (fare ya da klavye odağı) ve "şu an basılamam" (devre dışı). Her biri bir önek. Aynı düğmeyi sayfanın birçok yerinde kullanacağın için onu baştan bir bileşen yap: proje kökünde components klasörü ve içine Dugme.tsx:

components/Dugme.tsx — stil-atolyesi — Visual Studio Code
components/Dugme.tsx
const turSiniflari = {    birincil: "bg-violet-700 text-white hover:bg-violet-600",    ikincil: "border border-zinc-500 bg-white text-zinc-900 hover:bg-zinc-100 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800",    tehlike: "bg-red-700 text-white hover:bg-red-600",} type DugmeProps = {    tur?: "birincil" | "ikincil" | "tehlike"    type?: "button" | "submit"    disabled?: boolean    onClick?: () => void    children: React.ReactNode} export default function Dugme({ tur = "birincil", type = "button", disabled = false, onClick, children }: DugmeProps) {    return (        <button            type={type}            disabled={disabled}            onClick={onClick}            className={`cursor-pointer rounded-lg px-4 py-2 font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500 disabled:cursor-not-allowed disabled:opacity-50 ${turSiniflari[tur]}`}        >            {children}        </button>    )}
Ln 1, Col 1Spaces: 4UTF-8TSX

Uzun className satırları Tailwind'de olağan. VS Code'da uzun satırları pencereye sığdırmak için View → Word Wrap (Alt+Z).

Parça parça:

  • turSiniflari: düğmenin üç çeşidi (birincil, ikincil, tehlike), her biri için tam yazılmış bir sınıf listesi (boşlukla ayrılmış sınıf adlarından oluşan tek bir metin). turSiniflari[tur] seçilen çeşidin metnini verir (Ders 4'teki köşeli parantezle alan okuma). Neden renk adını değişkenden kurmadığımızı birazdan bir hatayla göreceksin.
  • ReactNode: React'in çizebildiği her şeyin tipi (Ders 40). children düğmenin içine yazdığın şey: <Dugme>Kaydet</Dugme>'de "Kaydet".
  • tur = "birincil", type = "button": varsayılan parametreler. type önemli: form içindeki bir <button>'ın türü yazılmazsa submit sayılır ve her basışta formu gönderir. Göndermesi gereken düğmeye type="submit"'i bilerek vereceksin.
  • cursor-pointer: Tarayıcılar düğmenin üstünde sıradan ok imlecini gösterir; Tailwind 4 de bunu değiştirmez (Tailwind 3 değiştiriyordu). El imleci "buraya basılır" ipucunu verir.
  • hover:bg-violet-600: fare üstündeyken bir ton açık. Tailwind 4 hover:'ı yalnız fareyle gezilen cihazlarda çalıştırır; telefonda parmakla dokununca düğme açık tonda takılı kalmaz.
  • focus-visible:outline-2 …: klavyeyle gelindiğinde 2 piksel kalınlığında, 2 piksel dışarıda, mor bir çerçeve. Ders 25'teki :focus-visible ile aynı: fareyle tıklayınca çıkmaz, Tab ile gelince çıkar.
  • disabled:opacity-50 disabled:cursor-not-allowed: disabled olan düğme yarı saydam görünür ve üstüne gelince yasak imleci çıkar. Tarayıcı devre dışı düğmeye basılmasına zaten izin vermez; bu sınıflar bunu gözle görünür yapar.

Dört çeşidi yan yana görmek için page.tsx'te paragrafın altına geçici olarak şunu koy (en üste import Dugme from "@/components/Dugme"):

TSX
<div className="mt-6 flex flex-wrap gap-3">    <Dugme>Kaydet</Dugme>    <Dugme tur="ikincil">Vazgeç</Dugme>    <Dugme tur="tehlike">Sil</Dugme>    <Dugme disabled>Kaydediliyor…</Dugme></div>

Sayfayı yenile ve fareye dokunmadan bir kez Tab'a bas:

localhost:3000
Yan yana dört düğme: mor zeminli beyaz yazılı Kaydet, çevresinde ikinci bir mor çerçeve; beyaz zeminli gri kenarlı Vazgeç; kırmızı zeminli Sil; soluk mor Kaydediliyor.
Dört düğme. İlki klavye odağında: çevresinde mor çerçeve var. Sonuncusu devre dışı, soluk.

Fareyi düğmelerin üstünde gezdir: renkler bir ton açılıyor. Son düğmenin üstünde imleç yasak işaretine dönüyor ve Tab onu atlıyor: devre dışı düğme odak almaz. Düğmeleri gördükten sonra bu div'i sil; düğmeyi formda ve hata ekranında kullanacaksın.

Sınıf adını parça parça kurmak

Düğmeye yeni bir renk eklemek istediğini düşün: onay için yeşil. Akla gelen ilk yol, renk adını tutan bir obje ve şablon metinle kurulan bir sınıf. Önce Node'da ne ürettiğine bak. C:\kod\ogren\ders45.ts dosyasına yaz ve o klasörde node ders45.ts ile çalıştır:

ders45.ts
const renk = "emerald"const sinif = `bg-${renk}-700`console.log(sinif)
Çıktı
bg-emerald-700

JavaScript doğru metni üretiyor. Şimdi aynı fikri bir bileşende dene:

Sık hataSınıf adını parça parça kurmak: bg-${renk}-700

Ne zaman çıkar? Sınıf adının bir parçasını değişkenden, prop'tan ya da şablon metinle kurunca. Şu bileşeni components/RenkliDugme.tsx olarak yaz ve sayfada iki kez kullan: <RenkliDugme tur="birincil">Kaydet</RenkliDugme> ve <RenkliDugme tur="onay">Onayla</RenkliDugme>.

components/RenkliDugme.tsx✗ Yanlış
const renkler = {    birincil: "violet",    onay: "emerald",} type RenkliDugmeProps = {    tur: "birincil" | "onay"    children: React.ReactNode} export default function RenkliDugme({ tur, children }: RenkliDugmeProps) {    return (        <button className={`cursor-pointer rounded-lg px-4 py-2 font-semibold text-white bg-${renkler[tur]}-700`}>            {children}        </button>    )}

Ne terminal ne tarayıcı hata verir. Ama sayfada şu çıkar:

localhost:3000
Solda mor zeminli Kaydet düğmesi. Sağında Onayla yazısı, zemini olmadığı için beyaz yazı neredeyse görünmüyor.
Kaydet mor. Onayla'nın zemini yok: beyaz yazı açık zeminde kayboldu.

Onayla'ya sağ tıkla, İncele de. Elements'te class="… bg-emerald-700" yazıyor, yani sınıf öğenin üstünde. Styles panelinde ise .bg-emerald-700 diye bir kural yok. Elements'te seçili öğe Console'da $0 adıyla durur; zeminine sor:

Konsol (F12 → Console)
> getComputedStyle($0).backgroundColor'rgba(0, 0, 0, 0)'

Saydam. Kaydet düğmesinde aynı soru 'lab(35.2783 67.9912 -88.793)' döndü: mor. Kaydet neden çalışıyor? Çünkü bg-violet-700 sözcüğü projede başka bir yerde, Dugme.tsx'te tam olarak yazılı; Tailwind onu orada görüp CSS'ini üretti. bg-emerald-700 ise hiçbir dosyada tam olarak geçmiyor; Tailwind yalnız bg-${renkler[tur]}-700 metnini okudu ve bundan bir sınıf çıkaramadı. Kodun çalışınca ne üreteceğini bilemez, çünkü kodu çalıştırmaz.

Bu yüzden bu hata sinsidir: aynı renk başka bir dosyada geçiyorsa her şey çalışır görünür, o dosyayı sildiğin gün düğme kaybolur.

Nasıl düzelir? Sınıf adını her zaman tam yaz; değişkenle yalnız hangi tam adın seçileceğini belirle. Dugme.tsx'teki turSiniflari tam olarak bunu yapıyor:

TSX✓ Doğru
const renkler = {    birincil: "bg-violet-700 hover:bg-violet-600",    onay: "bg-emerald-700 hover:bg-emerald-600",}

Denedikten sonra RenkliDugme.tsx'i ve sayfadaki iki satırı sil.

Kart: bir maçın bütün bilgisi

Listedeki ögeler şimdilik yalnız şampiyonun adını gösteriyor. Bir maç kartında sonuç, skor (öldürme / ölüm / asist) ve süre de olsun. components/MacKarti.tsx:

components/MacKarti.tsx
import type { Mac } from "@/lib/maclar" const sonucSiniflari = {    zafer: "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300",    bozgun: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300",} export default function MacKarti({ mac }: { mac: Mac }) {    const zafer = mac.sonuc === "zafer"    return (        <article className="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">            <div className="flex items-center justify-between gap-3">                <h2 className="text-lg font-bold">{mac.sampiyon}</h2>                <span className={`rounded-full px-2.5 py-0.5 text-sm font-semibold ${sonucSiniflari[mac.sonuc]}`}>                    <span aria-hidden="true">{zafer ? "✓ " : "✗ "}</span>                    {zafer ? "Zafer" : "Bozgun"}                </span>            </div>            <p className="mt-3 font-mono text-xl">{mac.oldurme} / {mac.olum} / {mac.asist}</p>            <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">{mac.dakika} dakika</p>        </article>    )}
  • Kartın kendisi: rounded-xl 12 piksel yuvarlak köşe, border border-zinc-200 ince açık gri kenar, shadow-sm hafif gölge, p-4 iç boşluk. Koyu temada kenar ve zemin koyulaşıyor.
  • Üst satır: flex items-center justify-between (Ders 21): ad solda, rozet sağda, ikisi dikeyde ortalı. gap-3 ad uzarsa ikisinin birbirine yapışmasını önler.
  • Rozet: rounded-full hap biçimi, px-2.5 py-0.5 küçük bir iç boşluk; 2.5 gibi buçuklu sayılar da ölçekte var (10 piksel). Renkler yine tam yazılmış sınıf listelerinden geliyor.
  • aria-hidden="true": ✓ ve ✗ işaretleri gözle bakan için. Ekran okuyucu onları "onay işareti" diye okumasın; zaten hemen yanında "Zafer" yazıyor. Bu küçük nitelik bu dersin ARIA bölümünde yeniden karşına çıkacak.
  • font-mono: skoru eş genişlikli yazı tipiyle yazar; rakamlar alt alta kartlarda hizalı durur.

page.tsx'te listeyi kartlara çevir: <li>'nin sınıflarını sil, içine kartı koy (en üste import MacKarti from "@/components/MacKarti"):

app/page.tsx
import MacKarti from "@/components/MacKarti"import { maclar } from "@/lib/maclar" export default function Home() {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <h1 className="text-3xl font-bold">Maç geçmişi</h1>            <p className="mt-1 text-zinc-600 dark:text-zinc-400">Son maçlarına bak, yenisini ekle.</p>            <ul className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">                {maclar.map((mac) => (                    <li key={mac.id}>                        <MacKarti mac={mac} />                    </li>                ))}            </ul>        </main>    )}

Telefon genişliğinde (390 px) kartlar alt alta, dizüstü genişliğinde (1280 px) üçerli dizilir:

Telefon genişliğinde sayfa: Maç geçmişi başlığı, altında açıklama, sonra alt alta tam genişlikte kartlar. Ahri kartında sağda yeşil Zafer rozeti, 8/2/11 skoru ve 31 dakika; Jinx kartında kırmızı Bozgun rozeti.
Şekil 45.4 Aynı sayfa 390 piksel genişlikte.
localhost:3000
Üç sütunlu ızgara: üstte Ahri (Zafer), Jinx (Bozgun), Garen (Zafer); altta Lux (Zafer). Her kartta skor ve süre.
1280 piksel genişlikte kartlar üç sütun. Dördüncü kart ikinci satırda.

Koyu temada da kontrol et (Rendering sekmesi ya da Windows ayarı):

localhost:3000
Koyu temada aynı sayfa, 1000 piksel genişlikte iki sütun. Siyah zemin üzerinde koyu gri kartlar, beyaz başlıklar, rozetlerde açık yeşil ve açık kırmızı yazı.
Koyu tema: zemin neredeyse siyah, kartlar koyu gri, rozetler koyu yeşil ve koyu kırmızı üstünde açık yazılı.

Form: etiket, girdi ve hata mesajı

Formun bir girdi kutusu var ve kullanıcı ona yazınca state değişecek; bu yüzden form bir istemci bileşeni olur. components/YeniMacFormu.tsx:

components/YeniMacFormu.tsx
"use client" import { useState } from "react"import Dugme from "@/components/Dugme" export default function YeniMacFormu() {    const [sampiyon, setSampiyon] = useState("")    const [hata, setHata] = useState("")    const [mesaj, setMesaj] = useState("")     function gonder(olay: React.SubmitEvent<HTMLFormElement>) {        olay.preventDefault()        if (sampiyon.trim() === "") {            setHata("Şampiyonun adını yaz.")            setMesaj("")            return        }        setHata("")        setMesaj(`${sampiyon} maçı kaydedildi.`)        setSampiyon("")    }     return (        <form id="yeni-mac" onSubmit={gonder} noValidate className="mt-10 max-w-md rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900">            <h2 className="text-xl font-bold">Yeni maç</h2>            <label htmlFor="sampiyon" className="mt-4 block font-medium">Şampiyon</label>            <input                id="sampiyon"                value={sampiyon}                onChange={(olay) => setSampiyon(olay.target.value)}                placeholder="Örn. Ahri"                required                aria-invalid={hata !== ""}                aria-describedby={hata ? "sampiyon-hata" : undefined}                className={`mt-1 block w-full rounded-lg border bg-white px-3 py-2 placeholder:text-zinc-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500 dark:bg-zinc-950 dark:placeholder:text-zinc-400 ${hata ? "border-red-700 dark:border-red-400" : "border-zinc-500"}`}            />            {hata && (                <p id="sampiyon-hata" role="alert" className="mt-1 text-sm text-red-700 dark:text-red-400">                    {hata}                </p>            )}            <label htmlFor="sonuc" className="mt-4 block font-medium">Sonuç</label>            <select id="sonuc" className="mt-1 block w-full rounded-lg border border-zinc-500 bg-white px-3 py-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500 dark:bg-zinc-950">                <option value="zafer">Zafer</option>                <option value="bozgun">Bozgun</option>            </select>            <div className="mt-6 flex items-center gap-3">                <Dugme type="submit">Kaydet</Dugme>                <p role="status" className="text-sm text-emerald-700 dark:text-emerald-400">{mesaj}</p>            </div>        </form>    )}

Mantık Ders 41'deki kontrollü girdiyle aynı; yeni olan görünüm ve erişilebilirlik ayrıntıları:

  • Etiket: Her kutunun gerçek bir <label htmlFor>'u var (JSX'te for yerine htmlFor, Ders 40). block etiketi kendi satırına alır; etikete tıklayınca imleç kutuya gider.
  • Kutu: Preflight kutunun kenarını sıfırlamıştı; border kenarı geri getirir, w-full kutuyu formun genişliğine yayar. Kenar rengi zinc-500: Ders 25'teki kurala göre bir girdi kutusunun kenarı zeminle en az 3:1 kontrast taşımalı. Beyaz zeminde zinc-500 4,83:1, daha açık zinc-400 ise 2,62:1'de kalır.
  • placeholder:text-zinc-500 dark:placeholder:text-zinc-400: placeholder: öneki, kutu boşken görünen silik yazıyı boyar. İpucu yazısının da okunabilir olması gerekir. Koyu temada bir ton açığını seçtik: dark:placeholder: iki önek üst üste, "koyu temadaysa ve ipucu yazısıysa" demek. Nedenini kontrast bölümünde ölçeceksin.
  • Hata hâli: Kenar rengi hataya göre değişiyor; dikkat et, iki renk de tam ad. border-zinc-500 ile border-red-700'ü aynı anda yazmak yerine biri seçiliyor: aynı özelliği ayarlayan iki sınıf bir arada olursa hangisinin kazanacağı className'deki sıraya değil, Tailwind'in ürettiği CSS dosyasındaki sıraya bağlıdır.
  • aria-invalid ve aria-describedby: İlki ekran okuyucuya "bu kutunun değeri geçersiz" der. İkincisi kutuyu altındaki hata paragrafına bağlar: kullanıcı kutuya geldiğinde ekran okuyucu etiketin ardından "Şampiyonun adını yaz." cümlesini de okur. HTML'de bu bağı kuran başka bir yol yok; ARIA'nın gerçekten gerektiği yerlerden biri.
  • role="alert" ve role="status": Ders 23–25'te gördüğün gibi: hata çıkınca hemen okunur, başarı mesajı değişince sırası gelince okunur.
  • required ve noValidate: required (Ders 23) kutunun zorunlu olduğunu ekran okuyucuya da söyler; kutuya gelen kullanıcı "gerekli" sözcüğünü duyar. noValidate ise tarayıcının kendi doğrulama balonunu kapatır: boş gönderimde tarayıcının "Lütfen bu alanı doldurun." (İngilizce tarayıcıda "Please fill out this field.") balonu yerine bizim tasarladığımız hata cümlesi çıkar ve gonder her basışta çalışır.

Aynı üç odak sınıfı girdi kutusunda, açılır listede, düğmede ve birazdan yazacağın bağlantıda tekrar ediyor. Görev 3'te onları tek bir sabite toplayacaksın.

Formu sayfaya koy ve başlığın yanına forma götüren bir "Maç ekle" bağlantısı ekle. page.tsx'in son hâli:

app/page.tsx
import MacKarti from "@/components/MacKarti"import YeniMacFormu from "@/components/YeniMacFormu"import { maclar } from "@/lib/maclar" export default function Home() {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <div className="flex flex-wrap items-center justify-between gap-4">                <div>                    <h1 className="text-3xl font-bold">Maç geçmişi</h1>                    <p className="mt-1 text-zinc-600 dark:text-zinc-400">Son maçlarına bak, yenisini ekle.</p>                </div>                <a href="#yeni-mac" className="rounded-lg bg-violet-700 px-4 py-2 font-semibold text-white hover:bg-violet-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500">                    Maç ekle                </a>            </div>            <ul className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">                {maclar.map((mac) => (                    <li key={mac.id}>                        <MacKarti mac={mac} />                    </li>                ))}            </ul>            <YeniMacFormu />        </main>    )}

"Maç ekle" düğmeye benziyor ama bir bağlantı (<a>): sayfada bir yere götürüyor, bir iş yapmıyor. Kural: götüren şey bağlantı, iş yapan şey düğme. Ekran okuyucu ikisini farklı duyurur, klavyede Enter ikisinde de çalışır ama Space yalnız düğmede çalışır. Görünüş aynı olabilir; etiket doğru olmalı. #yeni-mac adresi sayfayı id="yeni-mac" olan forma kaydırır (Ders 19'daki sayfa içi bağlantı).

Boş formla Kaydet'e bas:

localhost:3000
Yeni maç formu: Şampiyon etiketi, altında kırmızı kenarlı boş bir kutu ve içinde soluk Örn. Ahri yazısı, kutunun altında kırmızı Şampiyonun adını yaz cümlesi. Sonuç etiketi ve Zafer seçili bir açılır liste. En altta mor Kaydet düğmesi.
Boş gönderilen form. Kutunun kenarı kırmızı, altında hata cümlesi var.

Hata yalnız kırmızı renkle değil, bir cümleyle de anlatılıyor. Bunun neden önemli olduğunu az sonra göreceksin.

Erişilebilirlik: dört denetim

Erişilebilirlik temellerini Ders 19'da, kontrol listesini Ders 25'te gördün. Tailwind ve React'te kurallar değişmez; yalnız onları bozmanın yeni yolları çıkar. Her bileşeni bitirdiğinde dört soruyu sor: her şeyin bir adı var mı, odak görünüyor mu, kontrast yetiyor mu, klavyeyle her yere ulaşılıyor mu?

Etiket: her şeyin bir adı olsun

Ekran okuyucu bir kutuya ya da düğmeye geldiğinde onun adını okur. Ad nereden gelir?

  • Girdi kutusunda: ona bağlı <label>. placeholder etiket değildir: kullanıcı yazmaya başlayınca kaybolur, kutuya ne yazdığını unutan biri geri dönüp bakamaz.
  • Düğmede ve bağlantıda: içindeki yazı. "Kaydet" yazan düğmenin adı "Kaydet".
  • Yalnız simgeli bir düğmede ad yoktur. Adı ya aria-label ile ya da gözle görünmeyen bir yazıyla verirsin:
TSX
<button type="button" aria-label="Aramayı temizle">    <svg aria-hidden="true" viewBox="0 0 24 24">…</svg></button> <button type="button">    <span className="sr-only">Aramayı temizle</span>    <svg aria-hidden="true" viewBox="0 0 24 24">…</svg></button>

sr-only Tailwind'in "yalnız ekran okuyucu için" sınıfı: yazıyı sayfada tutar ama 1×1 piksellik görünmez bir kutuya sıkıştırır. Gözle bakan görmez, ekran okuyucu okur. İki yol da aynı adı verir. Simgedeki aria-hidden="true" ise SVG'nin ayrıca okunmasını önler.

Resimlerde ad, alt metnidir (Ders 19). Bilgi taşıyan resme ne gösterdiğini yaz; yalnız süs olan resme alt="" ver ki ekran okuyucu atlasın. Ders 43'teki next/image'ın Image bileşeni alt'ı zorunlu tutar; yazmazsan tsc Property 'alt' is missing diye uyarır.

Odak: nerede olduğunu göster

Fareyi kullanamayan biri sayfada Tab ile gezer. Odak çerçevesi onun fare imlecidir. Bizim sayfamızda Tab sırası şöyle çıktı:

  1. "Maç ekle" bağlantısı
  2. Şampiyon kutusu
  3. Sonuç listesi
  4. Kaydet düğmesi

Geliştirme sunucusunda (npm run dev) beşinci Tab ekranın sol alt köşesindeki yuvarlak N düğmesine gider: o Next'in geliştirici aracı, yayın hâlinde (npm start) yoktur. Kartlarda tıklanacak bir şey yok; o yüzden Tab onları atlıyor. Sıra, öğelerin HTML'deki sırası; görsel olarak yerlerini değiştirsen bile (ör. flex-row-reverse) Tab HTML sırasını izler.

Sık hataOdak çerçevesini kaldırmak: outline-none

Ne zaman çıkar? İnternetteki hazır düğme örneklerinin çoğunda focus:outline-none ya da outline-none yazar: tarayıcının odak çerçevesi "çirkin" bulunup kapatılır. Dugme'deki üç focus-visible: sınıfını silip yerine outline-none yazarsan formda Tab'a dört kez bastığında şunu görürsün:

Formun alt kısmı iki kez, alt alta. Üsttekinde outline-none: odak Kaydet'te, ama iz yok yazıyor; Sonuç listesi ve Kaydet düğmesi sıradan görünüyor. Alttakinde focus-visible:outline-2: odak Kaydet'te yazıyor; Kaydet düğmesinin çevresinde biraz boşluk bırakılmış mor bir çerçeve var.
Şekil 45.5 Üstte outline-none: odak Kaydet'te ama hiçbir iz yok. Altta bizim düğmemiz.

Hata mesajı yok; Lighthouse da bunu yakalamaz. Fareyle gezen biri hiçbir şey fark etmez. Klavyeyle gezen biri ise Enter'a basınca neyin olacağını bilemez: imleci görünmeyen bir fareyle tıklamak gibi.

Nasıl düzelir? Çerçeveyi silme, kendi tarzında çiz: focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500. Tarayıcının çerçevesini illa gizlemen gerekiyorsa (ör. odağı başka bir yolla, kalın bir gölgeyle gösterdiğinde) outline-none yerine outline-hidden kullan: çerçeveyi normalde gizler ama Windows'un yüksek karşıtlık temalarında (Ayarlar → Erişilebilirlik → Karşıtlık temaları; İngilizce Windows'ta Settings → Accessibility → Contrast themes) yine çizer. O temalarda tarayıcı gölgeleri siler, renkleri de sistemin renkleriyle değiştirir; geriye odağın tek ipucu olarak çerçeve kalır.

Kontrast: okunabiliyor mu?

Ders 20'de normal yazı için en az 4,5:1, Ders 25'te kutu kenarları için en az 3:1 sınırını gördün. Bir ayrıntı daha: büyük yazıda (24 piksel ve üstü ya da yaklaşık 18,7 piksel ve üstü kalın) da 3:1 yeter, çünkü iri harf daha kolay seçilir. Bu derste kullandığımız renkleri, sayfada üstünde durdukları zeminle ölçtük:

YazıZeminOranSonuç
zinc-900 (ana yazı)zinc-50 (sayfa)16,97:1Geçer
zinc-600 (açıklama)zinc-50 (sayfa)7,40:1Geçer
zinc-400 (koyu temada açıklama)zinc-950 (koyu sayfa)7,59:1Geçer
beyazviolet-700 (düğme)7,30:1Geçer
beyazviolet-600 (düğmenin üstündeyken)5,89:1Geçer
emerald-800emerald-100 (Zafer rozeti)6,78:1Geçer
red-800red-100 (Bozgun rozeti)6,85:1Geçer
zinc-400 (açık temada açıklama)zinc-50 (sayfa)2,51:1Kalır
zinc-500 (koyu temada ipucu yazısı)zinc-950 (koyu kutu)4,12:1Kalır

Son iki satıra dikkat: aynı zinc-400, koyu zeminde rahatça geçerken açık zeminde kalıyor. Beyaz kutuda 4,83:1 ile geçen zinc-500 ise koyu kutuda kalıyor; formdaki ipucu yazısına koyu tema için dark:placeholder:text-zinc-400 (7,59:1) vermemizin sebebi bu. Kontrast bir rengin değil, bir çiftin özelliğidir; her rengi iki temada da kendi zeminiyle ölç. Devre dışı düğme (yarı saydam) bu kuralın dışındadır; basılamadığı için okunması beklenmez, ama yine de ne yazdığı ayırt edilebilmeli.

Ölçmek için Ders 20'deki yolu kullan: yazıya sağ tıkla, İncele de, Styles'ta renk karesine tıkla; renk seçicideki Contrast ratio satırı oranı gösterir.

Klavye: her şeye ulaşılıyor mu?

Fareyi bırak ve sayfayı yalnız klavyeyle kullan: Tab ile gez, Enter ve Space ile bas, açılır listede ok tuşlarıyla seç. Bir yere ulaşamıyorsan ya da odak bir yerde kayboluyorsa sorun var.

En sık sebep, tıklanacak şeyi div ile yapmaktır: <div onClick={…}>Sil</div>. Fareyle mükemmel çalışır. Ama div odak almaz; Tab onun üstünde durmaz, Enter ona ulaşmaz, ekran okuyucu onun bir düğme olduğunu bilmez. Ne ESLint ne Lighthouse bunu yakalar; Görev 3'te kendin göreceksin. Çözüm her zaman aynı: iş yapan şey <button type="button">, götüren şey <a href> ya da Next'in Link'i. İkisi de klavyeyi, odağı ve ekran okuyucu adını hazır getirir. Görünüşü Tailwind'le istediğin gibi değiştirebilirsin.

Bilgiyi yalnız renkle verme

Sık hataSonucu yalnız renkle göstermek

Ne zaman çıkar? Bir durumu yalnız renk farkıyla anlatınca. Örneğin kartta "Zafer/Bozgun" yazısını kaldırıp sonucu yalnız kartın sol kenarının rengiyle göstermek:

TSX✗ Yanlış
<article className={`rounded-xl border-l-8 bg-white p-4 shadow-sm ${zafer ? "border-emerald-600" : "border-red-600"}`}>

Erkeklerin yaklaşık 12'de birinde bir tür renk körlüğü var; en yaygını kırmızıyla yeşili ayırt edememek. Chrome bunu gösterebiliyor: Rendering sekmesinde Emulate vision deficiencies listesinden Achromatopsia (no color) (hiç renk görmeyen) ya da Deuteranopia (no green) seç. Kartlarımızdan ikisi (Ahri kazandı, Jinx kaybetti) şöyle göründü:

Dört sıra, her sırada Ahri ve Jinx kartı. Birinci sıra normal görüş: Ahri'nin sol kenarı yeşil, Jinx'inki kırmızı. İkinci sıra Deuteranopia: Ahri'nin kenarı gri-zeytin, Jinx'inki koyu sarı. Üçüncü sıra Achromatopsia: iki kenar da neredeyse aynı gri. Dördüncü sıra yazılı kartlar renksiz: rozetlerde onay işaretli Zafer ve çarpı işaretli Bozgun okunuyor.
Şekil 45.6 Yukarıdan aşağı: yalnız renkle anlatılan sonuç; aynı kartlar kırmızı-yeşil körlüğünde ve hiç renk görmeden; yazı ve işaret eklenmiş kartlar, yine renksiz.

Deuteranopia'da yeşil kenar gri-zeytine, kırmızı kenar koyu sarıya döndü: iki kart yan yana dururken fark seçiliyor, ama tek bir karta bakan hangisinin kazandığını renkten çıkaramaz. Hiç renk görmeyen için iki kenar neredeyse aynı gri. Hata mesajı yok, Lighthouse da uyarmıyor. Formdaki kırmızı kenar da aynı tuzak olurdu; o yüzden altına bir hata cümlesi yazdık.

Nasıl düzelir? Rengi bırakma, yanına ikinci bir işaret ekle: yazı ("Zafer"), simge (✓ ✗), biçim (dolu ve boş yıldız) ya da desen. Renk görenler için hız, görmeyenler için bilgi.

ARIA: ne zaman, ne zaman değil

Bu derste birkaç aria- niteliği kullandın. Hepsi aynı ailenin üyesi:

ARIA güçlüdür ve tam bu yüzden tehlikelidir: yanlış ARIA, hiç ARIA olmamasından kötüdür. W3C'nin ARIA kullanım notlarındaki ilk kural şu: doğru HTML öğesi varsa onu kullan, ARIA ekleme.

YanlışNedenDoğru
<div role="button" tabIndex={0} onClick={…}>Klavyeyle basma, odak, disabled hepsini elle yazman gerekir<button type="button">
<span role="heading">Maçlar</span>Başlık sırası ve stil kayıp<h2>Maçlar</h2>
<button aria-label="düğme">Kaydet</button>aria-label görünen yazıyı ezer; ekran okuyucu "düğme" der<button>Kaydet</button>
<div aria-hidden="true"> içinde bir <button>Düğme klavyeyle odak alır ama ekran okuyucu için yokturGizleyeceksen tamamen gizle ya da hiç gizleme
Her şeye aria-labelGörünen yazıyla duyulan yazı ayrışırÖnce görünen yazıyı düzelt

Gerçekten gerektiği yerler ise bu derste gördüklerin: yalnız simgeli düğmenin adı (aria-label), hatayı kutuya bağlamak (aria-describedby, aria-invalid), değişen mesajı duyurmak (role="alert", role="status", aria-live), aç-kapa düğmesinin durumu (aria-pressed) ve süs öğesini gizlemek (aria-hidden). Bir de menüde o an açık olan sayfanın bağlantısı: Ders 43'ün menü görevinde onu yalnız beyaz renkle (.aktif) ayırmıştın; bağlantıya aria-current="page" eklersen ekran okuyucu "geçerli sayfa" der. Bu da "bilgiyi yalnız renkle verme" kuralının ARIA'lı hâli.

Sık hataaria-hiden: This attribute is an invalid ARIA attribute.

Ne zaman çıkar? Bir ARIA niteliğinin adını yanlış yazınca. MacKarti.tsx'te aria-hidden'ı aria-hiden yaz ve npm run lint çalıştır:

Terminal — PowerShell
PS C:\kod\stil-atolyesi> npm run lint > stil-atolyesi@0.1.0 lint> eslint  C:\kod\stil-atolyesi\components\MacKarti.tsx  15:27  warning  aria-hiden: This attribute is an invalid ARIA attribute. Did you mean to use aria-hidden?  jsx-a11y/aria-props ✖ 1 problem (0 errors, 1 warning)

"aria-hiden geçersiz bir ARIA niteliği. aria-hidden mı demek istedin?" Tarayıcı tanımadığı niteliği sessizce yok sayar; ✓ işareti yine okunur. TypeScript de yakalamaz: JSX, içinde tire olan her nitelik adını kabul eder; npx tsc --noEmit bu dosyada hiçbir şey söylemedi. Yakalayan, create-next-app'in kurduğu ESLint'in erişilebilirlik kuralları (jsx-a11y). 15:27 15. satırın 27. sütunu demek.

Nasıl düzelir? Adı düzelt: aria-hidden. ARIA nitelikleri yazdığın her projede commit'ten önce npm run lint çalıştırmayı alışkanlık edin.

Lighthouse ile ölçmek

Şimdiye kadar gözle ve klavyeyle denetledin. Bir de makineye ölçtür. Lighthouse, Chrome'un (Edge ve Brave'de de var) geliştirici araçlarına gömülü bir denetçi: sayfayı yükler, onlarca kurala karşı sınar ve her kategoriye 0 ile 100 arası bir puan verir.

  1. Yayın hâlini çalıştır. Hız puanı geliştirme sunucusunda anlamsızdır; Next orada her şeyi yavaş, denetimli derler. Sunucuyu Ctrl+C ile durdur, npm run build ve ardından npm start yaz. Sayfa yine http://localhost:3000'de açılır.
  2. Gizli pencere aç. Ctrl+Shift+N. Tarayıcı eklentileri sayfaya kod ekleyip sonucu bozabilir; gizli pencerede çoğu eklenti çalışmaz.
  3. Lighthouse sekmesini bul. F12, üstteki sekmelerden Lighthouse. Görünmüyorsa sekmelerin sağındaki » menüsünde.
  4. Ayarları seç. Mode: Navigation (sayfayı baştan yükleyerek ölçer). Device: Mobile (telefon gibi yavaş bir bağlantı ve küçük ekran taklit eder) ya da Desktop. Categories: dördü de işaretli kalsın.
  5. Çalıştır. Analyze page load düğmesine bas ve 30–60 saniye bekle. Türkçe arayüzde adlar çevrilmiş olabilir; Chrome sürümüne göre düğmelerin yeri değişmiş olabilir.

Rapor en üstte dört daire gösterir. Her biri bir kategori; renk, puanın hangi aralıkta olduğunu söyler:

99 Performans sayfa ne kadar hızlı açılıyor 86 Erişilebilirlik herkes kullanabiliyor mu 100 En İyi Uygulamalar güvenli ve güncel yöntemler mi 100 SEO arama motorları sayfayı anlıyor mu 0–49: zayıf 50–89: iyileştirilmeli 90–100: iyi Erişilebilirlik puanı, geçilen ve kalınan kuralların ağırlıklı ortalamasıdır.
Şekil 45.7 Lighthouse'un dört kategorisi ve puan renkleri. Sayılar, birazdan bilerek bozacağımız sayfanın (tablonun son satırı) açık temadaki gerçek sonucu.

Erişilebilirlik puanında yarım not yok: bir kural ya geçilir ya kalınır. Bir sayfada on düğmeden biri adsızsa "düğmelerin adı var" kuralı tümüyle kalır. Her kuralın bir ağırlığı vardır; ağır kurallardan kalmak puanı daha çok düşürür. Yeni Chrome sürümlerinde beşinci bir kategori, Agentic Browsing (Türkçesi Ajan Tabanlı Tarama: yapay zekâ ajanlarının sayfayı ne kadar rahat kullanabildiği) görebilirsin; Lighthouse onu hâlâ geliştirme aşamasında diye işaretliyor, bu derste ona bakmıyoruz.

Bilerek bozup ölçmek

Sayfamız iki temada da dört kategoride 99–100 aldı (Performans 99, diğerleri 100). Neyi yakaladığını görmek için üç şeyi bilerek boz: layout.tsx'te lang="tr"'yi sil, page.tsx'te açıklamanın sınıfını yalnız text-zinc-400 yap, formda Şampiyon'un <label> satırını sil. Yeniden derleyip ölçtüğümüzde:

SürümAçık temaKoyu temaLighthouse'un bulduğu
Düzgün sayfa100100—
Üç bozma9296açıkta: kontrast ve lang; koyuda: yalnız lang
Üç bozma + placeholder da silindi8690yukarıdakiler ve etiket

Kalınan kuralların raporda göründüğü adlar (Türkçe arayüzde ikincisi):

İngilizceTürkçe
Background and foreground colors do not have a sufficient contrast ratio.Arka plan ve ön plan renkleri yeterli kontrast oranına sahip değil.
<html> element does not have a [lang] attribute<html> öğesinin [lang] özelliği yok
Form elements do not have associated labelsForm öğelerinin ilişkili etiketleri yok

Her satırı açınca sorunlu öğe ve açıklaması çıkar. Kontrastta Lighthouse oranı da yazdı: Element has insufficient color contrast of 2.51 (foreground color: #9f9fa9, background color: #fafafa …). Expected contrast ratio of 4.5:1. Tablodaki 2,51 ile aynı.

Bu tablodan üç ders çıkıyor:

  • Lighthouse, sayfayı bilgisayarının o anki temasında ölçer. Windows'un koyu temadaysa açık temadaki kontrast hatasını görmez. Önemli sayfaları iki temada da ölç: Windows'un modunu değiştirip Lighthouse'u yeniden çalıştır.
  • Lighthouse placeholder'ı ad sayar. Etiketi sildik, Lighthouse sustu; çünkü kutunun hâlâ "Örn. Ahri" diye bir adı var. Gözle bakan kullanıcı ise yazmaya başlayınca kutunun ne için olduğunu göremez. Kuralı geçmek, sorunu çözmek değil.
  • 100 puan "erişilebilir" demek değildir. Odak çerçevesini silmek, div'e onClick vermek, bilgiyi yalnız renkle vermek bu derste hiçbir puanı düşürmedi. Erişilebilirlik bölümünün altında Additional items to manually check (Türkçe arayüzde Manuel olarak kontrol edilecek ek öğeler) başlığıyla on maddelik bir liste var: etkileşimli öğeler klavyeyle odaklanabiliyor mu, sekme sırası mantıklı mı, odak bir yerde tuzağa düşüyor mu… Puanı etkilemezler; onları sen denetlersin.

Boş, yükleniyor ve hata durumları

Ders 24'te bir sayfanın dört durumunu gördün: yükleniyor, dolu, boş, hata. Orada her durum tek satırlık bir yazıydı. İyi bir arayüz bundan fazlasını yapar: her durumda kullanıcıya ne olduğunu ve ne yapabileceğini söyler. Bu bölümde maçları bir "veritabanından" bekleyen sayfa yapacak ve üç durumu tasarlayacaksın.

Önce hazırlık. Başlık üç ekranda da (sayfa, yükleniyor, hata) aynı kalacak; üç yere kopyalamak yerine bir bileşene taşı. components/SayfaBasi.tsx:

components/SayfaBasi.tsx
export default function SayfaBasi() {    return (        <div className="flex flex-wrap items-center justify-between gap-4">            <div>                <h1 className="text-3xl font-bold">Maç geçmişi</h1>                <p className="mt-1 text-zinc-600 dark:text-zinc-400">Son maçlarına bak, yenisini ekle.</p>            </div>            <a href="#yeni-mac" className="rounded-lg bg-violet-700 px-4 py-2 font-semibold text-white hover:bg-violet-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500">                Maç ekle            </a>        </div>    )}

Sonra lib/maclar.ts'in sonuna, veritabanını taklit eden bir fonksiyon ekle. Fonksiyonun ilk satırı Ders 44'teki bekle ile aynı işi görür: bir buçuk saniye bekler. Ardından verilen durum'a göre hata fırlatır ya da boş liste döndürür; böylece üç durumu da istediğin an görebileceksin:

lib/maclar.ts
export async function maclariGetir(durum?: string) {    await new Promise((coz) => setTimeout(coz, 1500))    if (durum === "hata") {        throw new Error("Veritabanına bağlanılamadı")    }    if (durum === "bos") {        return []    }    return maclar}

durum'u adresten okuyacağız: localhost:3000/?durum=bos gibi. Adresin ?'ten sonraki kısmını (Ders 18) Next sayfaya searchParams prop'uyla verir. Ders 43'teki params gibi bu da bir Promise; await ile açarsın. page.tsx'in son hâli:

app/page.tsx
import BosDurum from "@/components/BosDurum"import MacKarti from "@/components/MacKarti"import SayfaBasi from "@/components/SayfaBasi"import YeniMacFormu from "@/components/YeniMacFormu"import { maclariGetir } from "@/lib/maclar" export default async function Home({ searchParams }: PageProps<"/">) {    const { durum } = await searchParams    const liste = await maclariGetir(typeof durum === "string" ? durum : undefined)    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <SayfaBasi />            {liste.length === 0 ? (                <BosDurum />            ) : (                <ul className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">                    {liste.map((mac) => (                        <li key={mac.id}>                            <MacKarti mac={mac} />                        </li>                    ))}                </ul>            )}            <YeniMacFormu />        </main>    )}
  • PageProps<"/">: Next'in senin için ürettiği tip; içinde searchParams: Promise<Record<string, string | string[] | undefined>> var. ?durum=bos&durum=hata gibi aynı ad iki kez yazılırsa değer dizi olur, hiç yazılmazsa undefined.
  • typeof durum === "string": daraltma. Yalnız düz metinse fonksiyona veriyoruz.
  • BosDurum, loading.tsx ve error.tsx'i aşağıda yazacaksın. BosDurum.tsx dosyası yokken sayfayı açarsan Next onu bulamadığı için hata gösterir; üçünü de yazdıktan sonra dene.

Yükleniyor: iskelet

Sayfa artık 1,5 saniye bekliyor. Bu sürede ne görünecek? Ders 44'te loading.tsx'e bir "yükleniyor" yazısı ve üç gri kutu koymuştun. Yalnız yazı iki kusur taşır: kullanıcı ne geleceğini bilmez ve veri gelince sayfa birden zıplar. Gri kutular ikisini de çözebilir, ama yalnız gelecek içerikle aynı boyda olurlarsa. Bu bölümde iskeleti ölçüp tasarlayacaksın.

app/loading.tsx:

app/loading.tsx
import SayfaBasi from "@/components/SayfaBasi" export default function Yukleniyor() {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <SayfaBasi />            <p role="status" className="sr-only">Maçlar yükleniyor…</p>            <ul aria-hidden="true" className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">                {[1, 2, 3].map((n) => (                    <li key={n} className="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">                        <div className="animate-pulse motion-reduce:animate-none">                            <div className="flex items-center justify-between">                                <div className="h-7 w-24 rounded bg-zinc-200 dark:bg-zinc-700" />                                <div className="h-6 w-16 rounded-full bg-zinc-200 dark:bg-zinc-700" />                            </div>                            <div className="mt-3 h-7 w-32 rounded bg-zinc-200 dark:bg-zinc-700" />                            <div className="mt-1 h-5 w-20 rounded bg-zinc-200 dark:bg-zinc-700" />                        </div>                    </li>                ))}            </ul>        </main>    )}
  • Aynı iskelet: Başlık gerçek SayfaBasi; zaten bildiğimiz şeyi gri kutuyla göstermeye gerek yok. Kartlar gerçek kartın kabuğu (kenar, zemin, p-4), içleri gri çubuklar. Çubukların yükseklikleri gerçek satırların yüksekliğine göre seçildi: h-7 (28 piksel) text-lg başlığın, h-5 (20 piksel) text-sm sürenin satır yüksekliği. Ölçtük: iskelet kartı da gerçek kart da 126 piksel yüksekliğinde. Veri gelince hiçbir şey kaymıyor.
  • animate-pulse: Tailwind'in hazır animasyonu; kutular yavaşça solup koyulaşır, "bir şey oluyor" der. motion-reduce:animate-none ise Windows'ta Ayarlar → Erişilebilirlik → Görsel efektler → Animasyon efektleri'ni (İngilizce Windows'ta Settings → Accessibility → Visual effects → Animation effects) kapatan kullanıcılarda animasyonu durdurur; bunu genelde hareket görünce başı dönen ya da dikkati dağılan kişiler kapatır. Bu tercihi tarayıcı prefers-reduced-motion ile bildirir; Rendering sekmesinden de taklit edebilirsin.
  • Ekran okuyucu için: Gri kutuların okunacak bir şeyi yok; aria-hidden="true" onları gizler. Yerine sr-only bir role="status" paragrafı "Maçlar yükleniyor…" der.

Sayfayı yenile:

localhost:3000
Maç geçmişi başlığı ve Maç ekle düğmesi gerçek. Altında iki sütunlu ızgarada üç beyaz kart; her kartın içinde solda iki gri çubuk, sağ üstte hap biçimli gri bir kutu, altta daha kısa bir gri çubuk.
İlk 1,5 saniye: gerçek başlık ve üç iskelet kart.

Lighthouse bu sayfaya Performans'tan 99 ile 100 arası puan verdi; 1,5 saniyelik beklemeye rağmen. Çünkü sayfanın ilk görünen hâli (başlık ve iskelet) beklemeden geliyor ve sonra hiçbir şey kaymıyor.

Boş durum: yol göster

localhost:3000/?durum=bos'u aç. Liste boş olunca kullanıcıya boş bir alan değil, bir sonraki adımı göster.

components/BosDurum.tsx:

components/BosDurum.tsx
export default function BosDurum() {    return (        <div className="mt-6 rounded-xl border-2 border-dashed border-zinc-300 px-6 py-12 text-center dark:border-zinc-700">            <svg aria-hidden="true" viewBox="0 0 24 24" className="mx-auto size-12 text-zinc-500" fill="none" stroke="currentColor" strokeWidth="1.5">                <rect x="4" y="3" width="16" height="18" rx="2" />                <path d="M8 8h8M8 12h8M8 16h5" />            </svg>            <h2 className="mt-4 text-lg font-bold">Henüz maç yok</h2>            <p className="mt-1 text-zinc-600 dark:text-zinc-400">Oynadığın maçları ekledikçe burada kart kart görünecek.</p>            <a href="#yeni-mac" className="mt-6 inline-block rounded-lg bg-violet-700 px-4 py-2 font-semibold text-white hover:bg-violet-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500">                İlk maçını ekle            </a>        </div>    )}

border-dashed kesik kenar "burası henüz boş bir yer" der. size-12 simgenin hem genişliğini hem yüksekliğini 48 piksel yapar; stroke="currentColor" SVG'nin çizgisini text-zinc-500'ün rengine bağlar. Simge süs; adı yok, aria-hidden'la gizli.

localhost:3000/?durum=bos
Maç geçmişi başlığının altında kesik gri kenarlı geniş bir kutu. Ortasında gri bir belge simgesi, Henüz maç yok başlığı, Oynadığın maçları ekledikçe burada kart kart görünecek açıklaması ve mor İlk maçını ekle düğmesi.
Boş durum: neden boş olduğu ve ne yapılacağı.

İki çeşit boşluğu karıştırma. "Hiç maç yok" ile "aramana uyan maç yok" farklı durumlardır: ilkinde "İlk maçını ekle" dersin, ikincisinde "Filtreyi temizle". İkisine aynı ekranı gösterirsen, filtresi açık kalmış kullanıcı bütün maçlarının silindiğini sanır.

Hata durumu: ne oldu, ne yapmalı?

localhost:3000/?durum=hata'yı aç. maclariGetir bir hata fırlatıyor; Next sayfanın yerine error.tsx'i çizer. Ders 44'teki gibi bu dosya bir istemci bileşenidir ve retry fonksiyonunu prop olarak alır: çağırınca Next sayfanın verisini sunucudan yeniden ister ve çizmeyi bir kez daha dener. Bu sefer error prop'unu almıyoruz; ekranda teknik ayrıntı göstermeyeceğiz.

app/error.tsx
"use client" import Dugme from "@/components/Dugme"import SayfaBasi from "@/components/SayfaBasi" export default function HataSayfasi({ retry }: { retry: () => void }) {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <SayfaBasi />            <div role="alert" className="mt-6 rounded-xl border border-red-300 bg-red-50 p-6 dark:border-red-900 dark:bg-red-950">                <h2 className="text-lg font-bold text-red-800 dark:text-red-200">Maçlar yüklenemedi</h2>                <p className="mt-1 text-red-800 dark:text-red-200">Sunucuya şu an ulaşamıyoruz. Bilgilerin silinmedi; birazdan yeniden dene.</p>                <div className="mt-4">                    <Dugme onClick={() => retry()}>Yeniden dene</Dugme>                </div>            </div>        </main>    )}
localhost:3000/?durum=hata
Maç geçmişi başlığının altında açık kırmızı zeminli, kırmızı kenarlı bir kutu: koyu kırmızı Maçlar yüklenemedi başlığı, Sunucuya şu an ulaşamıyoruz. Bilgilerin silinmedi; birazdan yeniden dene açıklaması ve mor Yeniden dene düğmesi.
Hata durumu. Sayfanın çerçevesi yerinde; ne olduğu ve ne yapılacağı yazıyor.

İyi bir hata ekranının dört özelliği:

  1. Sayfa çökmüş gibi görünmez. Başlık ve "Maç ekle" bağlantısı yerinde; kullanıcı nerede olduğunu biliyor.
  2. Ne olduğunu insan diliyle söyler. "Maçlar yüklenemedi", Error: ECONNREFUSED değil.
  3. Kaygıyı giderir. "Bilgilerin silinmedi" cümlesi, maç kayıtlarının kaybolduğunu sanan birini sakinleştirir.
  4. Bir çıkış yolu sunar. "Yeniden dene" düğmesi. Sorun geçiciyse tek basış yeter.

Teknik ayrıntıyı kullanıcıya göstermiyoruz. Yayın hâlinde Next zaten göstermez: sunucu bileşeninde fırlatılan hatanın mesajı tarayıcıya hiç gitmez. Bu denemeyi npm run build ve npm start ile yaptık; tarayıcının konsolunda yalnız kısaltılmış bir React satırı çıktı, gerçek hata sunucunun terminalindeydi:

Terminal — PowerShell
PS C:\kod\stil-atolyesi> npm start...⨯ Error: Veritabanına bağlanılamadı    at i (C:\kod\stil-atolyesi\.next\server\chunks\ssr\_1fsjxgo00dktp._.js:1:2254)    ... {  digest: '3517635533'}
Konsol (F12 → Console)
Error: Minified React error #441; visit https://react.dev/errors/441 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

digest hatanın kimliği: kullanıcı "bir hata aldım" dediğinde terminaldeki ya da barındırıcının loglarındaki (Ders 32) satırı bu sayıyla bulursun. Geliştirme sunucusunda (npm run dev) ise Next, hatayı ekranın köşesindeki kırmızı N düğmesinde de gösterir; o yalnız senin içindir.

Görevler

Görevleri C:\kod\stil-atolyesi projesinde yap. Her görev yeni bir sayfa ya da bileşen; dersin dosyalarını bozmadan yan yana durabilirler. Bitirince sayfayı hem açık hem koyu temada, hem telefon hem bilgisayar genişliğinde aç.

Görev 1

Rol rozetleri

Şampiyonların rolü için renkli rozetler istiyoruz. Önce mantığı Node'da kur: C:\kod\ogren\ders45.ts dosyasında rolSinifi(rol: string) adında bir fonksiyon yaz. Tank için bg-sky-100 text-sky-800, Suikastçı için bg-red-100 text-red-800, Büyücü için bg-violet-100 text-violet-800, Nişancı için bg-amber-100 text-amber-800, Destek için bg-emerald-100 text-emerald-800 döndürsün; tanımadığı her rol için gri bg-zinc-100 text-zinc-800. Fonksiyonu sırasıyla "Büyücü", "Nişancı", "Orman", "tank" (küçük harf) ve "" (boş metin) ile çağırıp her sonucu console.log ile yazdır. Beklenen çıktı:

Metin
bg-violet-100 text-violet-800bg-amber-100 text-amber-800bg-zinc-100 text-zinc-800bg-zinc-100 text-zinc-800bg-zinc-100 text-zinc-800

Sonra fonksiyonu export ile projeye, lib/roller.ts'e taşı ve rolü bir hap biçimli rozette gösteren bir RolRozeti bileşeninde kullan.

İpucu 1

Rolden sınıfa giden bir obje kur. Anahtarlarda Türkçe harf var; anahtarları tırnak içinde yaz: "Büyücü": "…". Objenin tipi Ders 42'deki Record<string, string> olabilir.

İpucu 2

Objede olmayan bir anahtarı okumak undefined verir. "Yoksa şunu kullan" demenin kısa yolu Ders 12'deki ??.

İpucu 3

Sınıfları bg-${renk}-100 diye kurma: o zaman Tailwind onları göremez. Her rolün tam sınıf listesi objede yazılı dursun.

Çözümü göster
ders45.ts
const rolSiniflari: Record<string, string> = {    "Tank": "bg-sky-100 text-sky-800",    "Suikastçı": "bg-red-100 text-red-800",    "Büyücü": "bg-violet-100 text-violet-800",    "Nişancı": "bg-amber-100 text-amber-800",    "Destek": "bg-emerald-100 text-emerald-800",} function rolSinifi(rol: string) {    return rolSiniflari[rol] ?? "bg-zinc-100 text-zinc-800"} console.log(rolSinifi("Büyücü"))console.log(rolSinifi("Nişancı"))console.log(rolSinifi("Orman"))console.log(rolSinifi("tank"))console.log(rolSinifi(""))
Çıktı
bg-violet-100 text-violet-800bg-amber-100 text-amber-800bg-zinc-100 text-zinc-800bg-zinc-100 text-zinc-800bg-zinc-100 text-zinc-800

"tank" gri çıktı: anahtarlar büyük-küçük harfe duyarlı. Veride roller hep büyük harfle başlıyorsa sorun yok; emin değilsen anahtarları tek biçime çevirmek gerekir (Türkçe harflerde toLocaleLowerCase("tr"), Ders 9).

Projeye taşınmış hâli; lib/roller.ts'te fonksiyonun başına export gelir, console.log satırları gelmez:

components/RolRozeti.tsx
import { rolSinifi } from "@/lib/roller" export default function RolRozeti({ rol }: { rol: string }) {    return <span className={`rounded-full px-2.5 py-0.5 text-sm font-semibold ${rolSinifi(rol)}`}>{rol}</span>}

Sınıf listeleri lib/roller.ts'te tam yazılı olduğu için Tailwind onları bulur. Beş rozetin hepsinde yazı, rozetin kendi açık zemininde 6:1'in üstünde kontrast taşır (en düşüğü Nişancı: 6,36:1); koyu tema için her listeye dark: karşılığını eklemek bir sonraki adım.

Görev 2

TFT dükkânı

app/dukkan/page.tsx adında bir sayfa aç. Beş birimlik bir TFT dükkânını ızgarada göster: telefonda 2, 640 pikselden itibaren 3, 1024 pikselden itibaren 5 sütun. Her birim beyaz (koyu temada koyu gri) bir kutu; içinde ortalanmış ad ve altında "N altın". Veri:

TypeScript
const birimler = [    { ad: "Garen", maliyet: 1 },    { ad: "Lux", maliyet: 2 },    { ad: "Jinx", maliyet: 3 },    { ad: "Teemo", maliyet: 4 },    { ad: "Annie", maliyet: 5 },]

Sınırları dene: geliştirici araçlarının cihaz çubuğunda genişliği 639, 640, 1023 ve 1024 yap.

İpucu 1

Öneksiz sınıf telefon içindir: grid-cols-2. Üstüne sm: ve lg: önekli iki sınıf eklenir.

İpucu 2

Ortalamak için text-center. Koyu tema için bg-white'ın yanına dark:bg-zinc-900, kenar rengine de bir dark: karşılığı.

Çözümü göster
app/dukkan/page.tsx
const birimler = [    { ad: "Garen", maliyet: 1 },    { ad: "Lux", maliyet: 2 },    { ad: "Jinx", maliyet: 3 },    { ad: "Teemo", maliyet: 4 },    { ad: "Annie", maliyet: 5 },] export default function Dukkan() {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <h1 className="text-2xl font-bold">Dükkân</h1>            <ul className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">                {birimler.map((birim) => (                    <li key={birim.ad} className="rounded-lg border border-zinc-200 bg-white p-3 text-center dark:border-zinc-800 dark:bg-zinc-900">                        <p className="font-semibold">{birim.ad}</p>                        <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">{birim.maliyet} altın</p>                    </li>                ))}            </ul>        </main>    )}

Ölçtüğümüz sütun sayıları:

GenişlikSütun
390 px2
639 px2
640 px3
1023 px3
1024 px5

640'ta üç sütun olunca beşinci birim ikinci satıra tek başına iner; 1024'te beşi tek satıra sığar. sm:grid-cols-3 yerine md:grid-cols-3 yazsaydın 640 ile 767 arasında iki sütun kalırdı.

Görev 3

Erişilebilirlik avı

Aşağıdaki bileşen bir şampiyon arama kutusu ve favori seçimi. Fareyle mükemmel çalışıyor ama içinde en az beş erişilebilirlik sorunu var. components/SampiyonArama.tsx olarak yaz, app/arama/page.tsx'ten çağır, sonra sorunları bul ve düzelt.

components/SampiyonArama.tsx✗ Yanlış
"use client" import { useState } from "react" const sampiyonlar = ["Ahri", "Annie", "Garen", "Jinx", "Lux", "Teemo"] export default function SampiyonArama() {    const [arama, setArama] = useState("")    const [favori, setFavori] = useState("")    const sonuclar = sampiyonlar.filter((ad) => ad.toLowerCase().includes(arama.toLowerCase()))     return (        <div className="max-w-sm rounded-xl bg-white p-6 dark:bg-zinc-900">            <div className="flex gap-2">                <input value={arama} onChange={(olay) => setArama(olay.target.value)} placeholder="Şampiyon ara" className="w-full rounded-lg border border-zinc-500 px-3 py-2 outline-none" />                <button type="button" onClick={() => setArama("")} className="cursor-pointer rounded-lg bg-violet-700 px-3 text-white outline-none">                    <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">                        <path d="M6 6l12 12M18 6L6 18" />                    </svg>                </button>            </div>            <p className="mt-2 text-sm text-zinc-400">Yazdıkça liste süzülür.</p>            <ul className="mt-4 space-y-2">                {sonuclar.map((ad) => (                    <li key={ad}>                        <div onClick={() => setFavori(ad)} className={`cursor-pointer rounded-lg px-3 py-2 ${favori === ad ? "bg-amber-200 dark:bg-amber-900" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}>                            {ad}                        </div>                    </li>                ))}            </ul>        </div>    )}
app/arama/page.tsx
import SampiyonArama from "@/components/SampiyonArama" export default function Arama() {    return (        <main className="mx-auto max-w-5xl px-4 py-8">            <h1 className="text-2xl font-bold">Favori şampiyonun</h1>            <div className="mt-4">                <SampiyonArama />            </div>        </main>    )}

Bitince Lighthouse'u açık ve koyu temada çalıştır (Erişilebilirlik 100 olmalı), sonra fareye dokunmadan bütün listeyi gez ve bir favori seç. space-y-2 yeni bir sınıf: liste ögelerinin arasına 8 piksel dikey boşluk koyar.

İpucu 1

Önce Lighthouse'u bozuk hâliyle çalıştır. Biz açık temada 89, koyu temada 94 aldık; iki sorun buldu. Kalanları dersin dört sorusuyla ara: ad, odak, kontrast, klavye. Bir de favorinin nasıl gösterildiğine bak.

İpucu 2

Fareyi bırakıp Tab'a bas. Bizde sıra kutu, çarpı düğmesi ve ardından tarayıcının adres çubuğu oldu (npm run dev açıksa araya köşedeki Next N düğmesi girer): liste hiç odak almadı. Odak çerçevesi de kutuda ve çarpı düğmesinde hiç görünmedi.

İpucu 3

Altı düzeltme: kutuya görünen bir label; çarpı düğmesine bir ad; outline-none yerine focus-visible: çerçevesi; ipucu yazısına daha koyu bir gri; div yerine aria-pressed'li bir button; favoriye renkten başka bir işaret (★ ve ☆). Odak sınıflarını üç kez yazmamak için onları bir sabite koy: const odak = "focus-visible:outline-2 …". Sabitte tam sınıf adları yazdığı için Tailwind onları görür.

Çözümü göster
components/SampiyonArama.tsx
"use client" import { useState } from "react" const sampiyonlar = ["Ahri", "Annie", "Garen", "Jinx", "Lux", "Teemo"]const odak = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500" export default function SampiyonArama() {    const [arama, setArama] = useState("")    const [favori, setFavori] = useState("")    const sonuclar = sampiyonlar.filter((ad) => ad.toLowerCase().includes(arama.toLowerCase()))     return (        <div className="max-w-sm rounded-xl bg-white p-6 dark:bg-zinc-900">            <label htmlFor="arama" className="block font-medium">Şampiyon ara</label>            <div className="mt-1 flex gap-2">                <input id="arama" value={arama} onChange={(olay) => setArama(olay.target.value)} aria-describedby="arama-ipucu" className={`w-full rounded-lg border border-zinc-500 px-3 py-2 ${odak}`} />                <button type="button" onClick={() => setArama("")} aria-label="Aramayı temizle" className={`cursor-pointer rounded-lg bg-violet-700 px-3 text-white ${odak}`}>                    <svg aria-hidden="true" viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">                        <path d="M6 6l12 12M18 6L6 18" />                    </svg>                </button>            </div>            <p id="arama-ipucu" className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">Yazdıkça liste süzülür.</p>            <ul className="mt-4 space-y-2">                {sonuclar.map((ad) => (                    <li key={ad}>                        <button type="button" onClick={() => setFavori(ad)} aria-pressed={favori === ad} className={`w-full cursor-pointer rounded-lg px-3 py-2 text-left ${odak} ${favori === ad ? "bg-amber-200 font-semibold dark:bg-amber-900" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"}`}>                            <span aria-hidden="true">{favori === ad ? "★ " : "☆ "}</span>                            {ad}                        </button>                    </li>                ))}            </ul>        </div>    )}
SorunLighthouse buldu mu?Düzeltme
Çarpı düğmesinin adı yokEvet: "Düğmelerin erişilebilir adları yok"aria-label="Aramayı temizle", SVG'ye aria-hidden
İpucu yazısı zinc-400 (beyazda 2,62:1)Yalnız açık temadatext-zinc-600 dark:text-zinc-400
Kutunun etiketi yok, yalnız placeholderHayır (placeholder'ı ad saydı)<label htmlFor="arama">
outline-none: odak görünmüyorHayırfocus-visible: sınıfları
div onClick: liste klavyeyle kullanılamıyorHayır<button type="button">
Favori yalnız sarı zeminle belliHayır★/☆ işareti, font-semibold, aria-pressed

Düzeltilmiş hâl iki temada da 100 aldı. Tab sırası: kutu, "Aramayı temizle", sonra altı şampiyonun düğmesi tek tek. Bir düğmede Space'e basınca yıldız doluyor ve ekran okuyucu "basılı" diyor. Lighthouse altı sorunun yalnız ikisini buldu; geri kalanını sen buldun.

Görev 4

Duyarlı ve koyu şampiyon kartı

Şu kart yalnız açık tema için ve her genişlikte yan yana tasarlanmış:

app/sampiyon/page.tsx✗ Yanlış
export default function Sampiyon() {    return (        <main className="mx-auto max-w-2xl px-4 py-8">            <article className="flex gap-4 rounded-xl border border-zinc-200 bg-white p-4">                <div aria-hidden="true" className="grid size-24 shrink-0 place-items-center rounded-lg bg-violet-700 text-4xl font-bold text-white">A</div>                <div>                    <h2 className="text-xl font-bold text-zinc-900">Ahri</h2>                    <p className="text-sm text-zinc-500">Dokuz Kuyruklu Tilki</p>                    <p className="mt-2 text-zinc-700">Ruhları büyüleyen, hızlı ve hareketli bir büyücü. Yakın dövüşe girip çıkmayı sever.</p>                </div>            </article>        </main>    )}

Değiştir: telefonda harf kutusu üstte, yazılar altında dursun; 640 pikselden itibaren yan yana geçsin. Koyu temada kart koyu, yazılar okunur olsun ve her yazı kendi zemininde en az 4,5:1 kontrast taşısın.

İpucu 1

flex öğeleri varsayılan olarak yan yana dizer. Telefon için öneksiz flex-col, 640'tan itibaren sm:flex-row.

İpucu 2

Koyu temada text-zinc-500'ün karşılığına dikkat: zinc-500 koyu kart zemininde (zinc-900) yalnız 3,67:1 verir. Bir ton açığını seç.

Çözümü göster
app/sampiyon/page.tsx
export default function Sampiyon() {    return (        <main className="mx-auto max-w-2xl px-4 py-8">            <article className="flex flex-col gap-4 rounded-xl border border-zinc-200 bg-white p-4 sm:flex-row dark:border-zinc-800 dark:bg-zinc-900">                <div aria-hidden="true" className="grid size-24 shrink-0 place-items-center rounded-lg bg-violet-700 text-4xl font-bold text-white">A</div>                <div>                    <h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">Ahri</h2>                    <p className="text-sm text-zinc-500 dark:text-zinc-400">Dokuz Kuyruklu Tilki</p>                    <p className="mt-2 text-zinc-700 dark:text-zinc-300">Ruhları büyüleyen, hızlı ve hareketli bir büyücü. Yakın dövüşe girip çıkmayı sever.</p>                </div>            </article>        </main>    )}

Ölçüm: flex-direction 390 ve 639 pikselde column, 640 pikselde row. Lighthouse'un erişilebilirlik puanının arkasındaki kural motoruyla (axe) iki temada da denetledik: kontrast sorunu yok. zinc-500 beyaz kartta 4,83:1 ile geçiyor; koyu kartta zinc-400 6,75:1. Harf kutusu süs olduğu için aria-hidden; ekran okuyucu "A, Ahri" yerine yalnız "Ahri" der. shrink-0, yan yana dizildiğinde uzun yazının kutuyu sıkıştırmasını önler.

Görev 5

Favoriler sayfasının üç durumu

/favoriler adresinde favori şampiyonları listeleyen bir sayfa yap; üç durumu da tasarla. Veri için lib/favoriler.ts:

lib/favoriler.ts
export type Favori = {    id: number    ad: string    rol: string} const favoriler: Favori[] = [    { id: 1, ad: "Ahri", rol: "Büyücü" },    { id: 2, ad: "Jinx", rol: "Nişancı" },    { id: 3, ad: "Garen", rol: "Tank" },] export async function favorileriGetir(durum?: string) {    await new Promise((coz) => setTimeout(coz, 1500))    if (durum === "hata") {        throw new Error("Favoriler okunamadı")    }    if (durum === "bos") {        return []    }    if (durum === "tek") {        return favoriler.slice(0, 1)    }    return favoriler}

İstenenler:

  • Dolu: Her favori bir satır: solda içinde adın baş harfi olan mor bir daire, yanında ad ve altında rol.
  • Yükleniyor: Satırların iskeleti. İskelet satırı gerçek satırla aynı yükseklikte olsun; hareketi azaltma tercihi olanlarda yanıp sönmesin; ekran okuyucu "Favoriler yükleniyor…" duysun.
  • Boş: Neden boş olduğunu ve ne yapılacağını söyleyen bir kutu; ana sayfaya giden "Şampiyonlara göz at" bağlantısıyla.
  • Hata: Ne olduğunu söyleyen, kaygıyı gideren bir mesaj ve "Yeniden dene" düğmesi.

?durum=bos, ?durum=tek ve ?durum=hata ile her durumu aç.

İpucu 1

Üç dosya: app/favoriler/page.tsx, app/favoriler/loading.tsx, app/favoriler/error.tsx. Sayfa searchParams'ı okur; tipi PageProps<"/favoriler">. Next bu tipi sayfayı ilk kez gördüğünde üretir; VS Code önce kızarsa npm run dev çalışırken dosyayı bir kez kaydet.

İpucu 2

Satır: flex items-center gap-3 … p-3. Daire: grid size-10 place-items-center rounded-full bg-violet-700 font-bold text-white. Ad (font-semibold, normal boyut) 24, rol (text-sm) 20 piksel yükseklik tutar; iskelette h-6 ve h-5 çubuklar kullan.

İpucu 3

Ana sayfaya bağlantı için <a href="/"> yazarsan npm run lint bir hata verir: "Do not use an <a> element to navigate to /. Use <Link /> from next/link instead." Ders 43'teki Link'i kullan. Hata ekranındaki düğme için dersin Dugme'si ve retry hazır.

Çözümü göster
app/favoriler/page.tsx
import Link from "next/link"import { favorileriGetir } from "@/lib/favoriler" export default async function Favoriler({ searchParams }: PageProps<"/favoriler">) {    const { durum } = await searchParams    const liste = await favorileriGetir(typeof durum === "string" ? durum : undefined)    return (        <main className="mx-auto max-w-md px-4 py-8">            <h1 className="text-2xl font-bold">Favori şampiyonların</h1>            {liste.length === 0 ? (                <div className="mt-4 rounded-xl border-2 border-dashed border-zinc-300 p-8 text-center dark:border-zinc-700">                    <h2 className="font-bold">Henüz favorin yok</h2>                    <p className="mt-1 text-zinc-600 dark:text-zinc-400">Bir şampiyonun yıldızına basınca burada görünür.</p>                    <Link href="/" className="mt-4 inline-block rounded-lg bg-violet-700 px-4 py-2 font-semibold text-white hover:bg-violet-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500">                        Şampiyonlara göz at                    </Link>                </div>            ) : (                <ul className="mt-4 space-y-3">                    {liste.map((favori) => (                        <li key={favori.id} className="flex items-center gap-3 rounded-xl border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900">                            <div aria-hidden="true" className="grid size-10 place-items-center rounded-full bg-violet-700 font-bold text-white">{favori.ad[0]}</div>                            <div>                                <p className="font-semibold">{favori.ad}</p>                                <p className="text-sm text-zinc-600 dark:text-zinc-400">{favori.rol}</p>                            </div>                        </li>                    ))}                </ul>            )}        </main>    )}
app/favoriler/loading.tsx
export default function FavorilerYukleniyor() {    return (        <main className="mx-auto max-w-md px-4 py-8">            <h1 className="text-2xl font-bold">Favori şampiyonların</h1>            <p role="status" className="sr-only">Favoriler yükleniyor…</p>            <ul aria-hidden="true" className="mt-4 space-y-3">                {[1, 2, 3].map((n) => (                    <li key={n} className="flex animate-pulse items-center gap-3 rounded-xl border border-zinc-200 bg-white p-3 motion-reduce:animate-none dark:border-zinc-800 dark:bg-zinc-900">                        <div className="size-10 rounded-full bg-zinc-200 dark:bg-zinc-700" />                        <div>                            <div className="h-6 w-24 rounded bg-zinc-200 dark:bg-zinc-700" />                            <div className="h-5 w-16 rounded bg-zinc-200 dark:bg-zinc-700" />                        </div>                    </li>                ))}            </ul>        </main>    )}
app/favoriler/error.tsx
"use client" import Dugme from "@/components/Dugme" export default function FavorilerHata({ retry }: { retry: () => void }) {    return (        <main className="mx-auto max-w-md px-4 py-8">            <h1 className="text-2xl font-bold">Favori şampiyonların</h1>            <div role="alert" className="mt-4 rounded-xl border border-red-300 bg-red-50 p-6 dark:border-red-900 dark:bg-red-950">                <h2 className="font-bold text-red-800 dark:text-red-200">Favorilerin yüklenemedi</h2>                <p className="mt-1 text-red-800 dark:text-red-200">Listen yerinde duruyor, yalnız şu an okuyamadık. Birazdan yeniden dene.</p>                <div className="mt-4">                    <Dugme onClick={() => retry()}>Yeniden dene</Dugme>                </div>            </div>        </main>    )}

Sınır denemeleri: iskelet satırı da gerçek satır da 70 piksel çıktı, veri gelince liste kaymadı. ?durum=tek tek satır gösterdi; ?durum=bos boş kutuyu, ?durum=hata hata kutusunu. Aynı kural motoruyla (axe) iki temada kontrast, etiket ve düğme adı denetimi temiz geçti. "Yeniden dene"ye ?durum=hata açıkken basarsan hata yine gelir; yapay hatamız kalıcı. Gerçek bir geçici kopmada ikinci deneme çoğu zaman başarır.

Kendini sına

Soru 1

Telefonda (390 px) bu başlık kaç piksel olur?

TSX
<h1 className="text-xl md:text-3xl">Maç geçmişi</h1>
Cevabı göster

20 piksel (text-xl). md:text-3xl ancak 768 pikselden itibaren devreye girer. Mobil öncelikli düzende öneksiz sınıf telefonun, önekli sınıf büyük ekranın kuralıdır.

Soru 2

Bu rozet bazen renkli, bazen renksiz çıkıyor. Neden? Nasıl düzeltirsin?

TSX
<span className={`bg-${sonuc === "zafer" ? "emerald" : "red"}-100`}>{sonuc}</span>
Cevabı göster

Tailwind dosyaları metin olarak okur, kodu çalıştırmaz. Bu satırda bg-emerald-100 ya da bg-red-100 diye tam bir sözcük yok; Tailwind bu sınıfların CSS'ini yalnız başka bir dosyada tam adlarını görürse üretir. "Bazen" çalışmasının sebebi bu: başka bir bileşen o sınıfı kullanıyorsa. Düzeltme: tam adları yaz, koşulla yalnız seçimi yap: sonuc === "zafer" ? "bg-emerald-100" : "bg-red-100".

Soru 3

Bu paragraf hangi renkte görünür?

TSX
<p className="text-zinc-600 text-red-700">Hata!</p>
Cevabı göster

zinc-600 (gri). İki sınıf aynı özelliği (color) ayarlıyor ve özgüllükleri eşit (Ders 20); kazanan, CSS dosyasında sonra yazılan. Tailwind'in ürettiği dosyada .text-red-700 kuralı .text-zinc-600'dan önce geliyor; className'deki sıranın hiçbir etkisi yok. Ders: aynı özellik için iki sınıf yazma; koşula göre birini seç.

Soru 4

Bir arkadaşın düğmesine focus:outline-none yazmış, "fareyle tıklayınca çıkan çerçeve çirkin" diyor. Ne önerirsin?

Cevabı göster

Çerçeveyi silmek klavyeyle gezenlerin odağı görmesini engeller. Çözüm focus-visible:: tarayıcı çerçeveyi fareyle tıklamada göstermez, klavyeyle gelince gösterir. focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500 gibi kendi tarzında bir çerçeve yaz; tarayıcınınkini gizlemek gerekiyorsa outline-none yerine outline-hidden kullan.

Soru 5

Sayfan Lighthouse'ta erişilebilirlikten 100 aldı. Sayfan erişilebilir mi?

Cevabı göster

Bilemezsin. Lighthouse yalnız makinenin ölçebildiği kuralları denetler. Bu derste odak çerçevesini silmek, div'e onClick vermek ve bilgiyi yalnız renkle vermek puanı hiç düşürmedi; placeholder'ı da etiket yerine saydı. Ayrıca yalnız o anki temada ölçer. 100, "makinenin bulabileceği sorun kalmadı" demektir; klavyeyle gezmek, iki temada bakmak ve raporun elle denetlenecek listesini yapmak sana kalır.

Soru 6

İskelet kartlarını neden gerçek kartla aynı yükseklikte yaptık? Sadece "Yükleniyor…" yazsak ne olurdu?

Cevabı göster

Veri gelince içerik iskeletin yerine oturur ve sayfa kaymaz. Tek satırlık bir yazı 24 piksel kadar yer kaplar; yerine 126 piksellik kartlar gelince altındaki her şey (form, bağlantılar) aşağı zıplar. Kullanıcı tam o anda bir şeye basmak üzereyse yanlış yere basar. Lighthouse bu kaymayı Performans'ta CLS ile ölçer ve puandan düşer.

Soru 7

Hangisi doğru: <button aria-label="düğme">Kaydet</button> mı, <button>Kaydet</button> mı?

Cevabı göster

İkincisi. Düğmenin adı zaten görünen yazısı: "Kaydet". aria-label o adı ezer; ekran okuyucu "Kaydet" yerine "düğme" der, gözle görülenle kulakla duyulan ayrışır. aria-label'ı yalnız görünen bir adı olmayan öğelerde (simgeli düğme gibi) kullan. Kural: doğru HTML yetiyorsa ARIA ekleme.

Kayıt noktası

create-next-app projeyi açarken Git deposunu kurup ilk kaydı kendisi aldı. Senin işini kaydetme sırası:

Bir sonraki ders Blok 6'nın projesi: Next.js ve PostgreSQL ile girişli, veritabanlı bir maç notları uygulaması yazıp yayına alacak, bu dersteki düğmeyi, kartı, formu ve durum ekranlarını orada gerçek veriyle kullanacaksın.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.