Flutter'da yazdığın widget ağacı, ekranı gören kullanıcı için mükemmel görünse de ekran okuyucu kullanan biri için tek başına hiçbir şey ifade etmez — VoiceOver ve TalkBack, Flutter'ın kendi Semantics ağacını okur, senin Container/Row/Column hiyerarşini değil. Bu Flutter erişilebilirlik rehberinde Semantics widget'ından MergeSemantics/ExcludeSemantics'e, dokunma hedefi ve kontrast kurallarından accessibility guideline test matcher'larına kadar gerçek cihazda çalışan bir kontrol listesi kuruyoruz.
💡 Pro Tip: Web hedefinde her yeni ekranı bitirir bitirmezflutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=trueile aç ve semantik ağacı gözünle gör; mobil veya genel bir alışkanlık istiyorsanMaterialApp(showSemanticsDebugger: true)işini görür — bu bakış, etiketi eksik veya yanlış birleşmiş düğümleri gözünle görmeni sağlar.
İçindekiler
- Semantics ağacı: Flutter ekranı ekran okuyucuya nasıl anlatır
- MergeSemantics, ExcludeSemantics ne zaman kullanılır
- Etiket, ipucu ve canlı bölge (live region)
- Görsel-dışı içerik: ikon-buton, grafik, boş durum
- Dokunma hedefi, kontrast ve metin ölçekleme
- Odak sırası ve klavye navigasyonu (web/masaüstü)
- Otomatik test: accessibility guideline matcher'ları
- Gerçek cihazda VoiceOver/TalkBack denetimi
- WCAG 2.2 ile eşleme ve teslim kontrol listesi
- SSS
- Flutter'da erişilebilirlik nasıl sağlanır?
- Semantics widget'ı ne işe yarar?
- Flutter uygulaması VoiceOver ile nasıl test edilir?
- Flutter'da dokunma hedefi boyutu kaç piksel olmalı?
- MergeSemantics ile ExcludeSemantics arasındaki fark nedir?
- Güncelleme (Eylül 2026)
- Sonuç
- Kaynaklar
Semantics ağacı: Flutter ekranı ekran okuyucuya nasıl anlatır
Flutter'ın standart widget'ları erişilebilirlik ağacını otomatik üretir; ancak uygulaman farklı bir davranış gerektirdiğinde bu ağaç Semantics widget'ıyla özelleştirilir. Semantics widget, widget ağacını anlam açıklamasıyla etiketler ve bu bilgi ekran okuyucular, arama motorları ve diğer semantik analiz yazılımları tarafından kullanılır. Yani senin gördüğün widget ağacı ile TalkBack/VoiceOver'ın okuduğu ağaç iki ayrı yapıdır; biri render için, diğeri anlam için var olur.
Semantics widget'ının container parametresi true verildiğinde, widget semantik ağacında yeni bir düğüm oluşturur — yani alt widget'ların semantiklerini kendi başına toplayan bağımsız bir birim haline gelir. Bunu her sarmalayıcıya körlemesine eklemek yerine, gerçekten "burada yeni bir anlamsal birim var" dediğin yerlerde kullanmalısın; aksi halde ekran okuyucu kullanıcısı aynı bilgiyi gereksiz yere parçalanmış halde duyar.
Web'de durum biraz farklı işler: ekran okuyucu kullanan ziyaretçilerin semantik ağacın kurulması için sayfadaki "Enable accessibility" düğmesini açması gerekir. Bunu bilmeyen bir ekip, "web'de VoiceOver hiçbir şey okumuyor" diye saatlerce debug yapabilir; oysa sorun kod değil, o tek düğmedir.
dart
1// Semantics ağacında yeni bir düğüm açar ve etiketler2Semantics(3 container: true,4 label: 'Profil kartı, Ayşe Yılmaz, kıdemli mühendis',5 child: ProfileCard(user: currentUser),6)MergeSemantics, ExcludeSemantics ne zaman kullanılır
MergeSemantics, alt widget'ların semantiklerini tek bir düğümde birleştiren bir widget'tır — klasik kullanım örneği bir checkbox ile yanındaki etiket metnini tek bir erişilebilir birim haline getirmektir. Bunu yapmazsan TalkBack kullanıcısı önce boş bir "onay kutusu" duyar, sonra ayrı bir dokunuşla yanındaki metni; MergeSemantics ikisini "Bildirimleri aç, onay kutusu" gibi tek ve anlamlı bir cümleye indirger.
ExcludeSemantics ise tam tersini yapar: alt ağacın tüm semantiğini erişilebilirlik ağacından düşürür. Flutter'ın kendi Material Chip widget'ı bunu tam olarak bu amaçla kendi içinde zaten yapar — chip içindeki avatar görseli etiket metninde tekrar ettiği için framework onu otomatik olarak gizler. Kuralı basit tut: eğer kendi bileşeninde bir alt widget'ın anlamı zaten üst düğümün etiketinde varsa, o alt widget'ı ExcludeSemantics ile sessizleştir.
dart
1// checkbox + etiketi tek erişilebilir birime indirger2MergeSemantics(3 child: Row(4 children: [5 Checkbox(value: notificationsOn, onChanged: _toggle),6 const Text('Bildirimleri aç'),7 ],8 ),9)10 11// Dekoratif ikonun yinelenen bilgisini gizler: Material Chip bunu kendi12// içinde zaten yapar, kendi bileşenlerinde aynı deseni sen kurarsın13Row(14 children: [15 ExcludeSemantics(child: Icon(Icons.star)),16 const Text('Öne çıkan'),17 ],18)Etiket, ipucu ve canlı bölge (live region)
Semantics widget'ının label özelliği erişilebilirlik amaçlı metin açıklaması sağlar, hint ise kullanıcıya ek bağlam ve rehberlik sunar — örneğin bir ikon-buton için label: 'Favorilere ekle', hint: 'Çift dokunarak ekler' gibi. liveRegion ise dinamik olarak güncellenen içeriği işaretler; bir hata mesajı, bir sepet toplamı veya bir yükleme durumu değiştiğinde bu düğüm ekran okuyucuya otomatik olarak duyurulur — kullanıcının o alana tekrar dokunmasını beklemeden.
Sesle okunması gereken metnin hangi dilde seslendirileceğini belirtmek için TextSpan.locale çağrılabilir; çok dilli bir uygulamada İngilizce bir marka adının Türkçe telaffuzla okunmasını önlemenin yolu budur. Semantics ağacını uygulama başlar başlamaz, ilk frame'den önce hazır etmek istiyorsan SemanticsBinding.instance.ensureSemantics() çağrısını runApp sonrasında main() içine ekleyebilirsin.
Platform eşlemesini ezbere bilmek işini kolaylaştırır: mobilde Android → TalkBack, iOS → VoiceOver; web masaüstünde macOS → VoiceOver, Windows → JAWS ve NVDA okur. Yani "ekran okuyucu testi yaptım" derken hangi cihaz/işletim sistemi kombinasyonunu kastettiğin, raporunda net olmalı — TalkBack'te sorunsuz çalışan bir liveRegion, NVDA'da farklı bir tempoda duyurulabilir.
Görsel-dışı içerik: ikon-buton, grafik, boş durum
Salt ikondan oluşan bir IconButton, Semantics etiketi olmadan ekran okuyucuya yalnızca "buton" olarak geçer — ne işe yaradığı belirsiz kalır. Kural basit: her ikon-buton, aksiyonu betimleyen bir label taşımalı ("Sil", "Favorilere ekle", "Menüyü aç" gibi), dekoratif olan ikonlar ise ExcludeSemantics ile ağaçtan düşürülmeli ki ekran okuyucu anlamsız gürültüyle zaman kaybettirmesin.
Grafik ve chart widget'ları da aynı mantıkla ele alınır: görsel eğrinin kendisi ekran okuyucuya hiçbir şey söylemez, ama etrafına eklenen bir Semantics(label: 'Son 7 günde satışlar %12 arttı') özet cümlesi aynı bilgiyi metin olarak taşır. Boş durum (empty state) ekranlarında da aynı disiplin geçerli: sadece bir illüstrasyon değil, "Henüz favori ürününüz yok" gibi bir semantik etiket olmalı — aksi halde ekran okuyucu kullanıcısı sayfanın boş mu yoksa yüklenmekte mi olduğunu ayırt edemez.
Yükleme (loading) durumları da aynı kör noktaya düşer: bir CircularProgressIndicator tek başına döndüğünde ekran okuyucu kullanıcısı ekranda bir şey olduğunu fark etmeyebilir, çünkü dönen bir spinner'ın kendisi ses çıkarmaz. Bu widget'ı Semantics(label: 'Yükleniyor', liveRegion: true) ile sarmalamak, yükleme başladığında ve bittiğinde AT kullanıcısına ayrı bir dokunuş gerektirmeden haber verir. Aynı prensip, sürükle-bırak listeler, karusel/slider bileşenleri ve özel çizilmiş (custom-painted) widget'lar için de geçerli: CustomPaint ile çizdiğin her şey görsel olarak zengin olabilir ama semantik ağaca hiçbir şey eklemez, dolayısıyla o widget'ı ayrı bir Semantics katmanıyla sarmalamayı unutursan ekran okuyucu kullanıcısı için o alan tamamen görünmez kalır. Pratikte ekip içinde şu kuralı yerleştirmek işe yarar: "görsel olarak yeni bir bilgi eklediysen, o bilginin bir semantik karşılığı da olmalı" — bu tek cümle, ikon-buton, grafik, boş durum ve yükleme göstergesi gibi dört farklı bileşen türünü tek bir disiplin altında toplar.
Dokunma hedefi, kontrast ve metin ölçekleme
Küçük dokunma hedefleri birçok kullanıcı için etkileşimi zorlaştırır ve seçimi güçleştirir — platform kılavuzları bu yüzden net minimum boyutlar tanımlar:
Platform | Minimum dokunma hedefi |
|---|---|
Android | 48×48 dp |
iOS | 44×44 pt |
W3C (web) | 44×44 CSS piksel |
Renk kontrastı için de benzer bir eşik var: küçük metin (18pt altı normal veya 14pt altı kalın) için en az 4.5:1, büyük metin için en az 3.0:1 kontrast oranı önerilir — bu, arayüzün aşırı aydınlık veya karanlık ortamlarda kullanılan cihazlarda dahi okunabilir kalmasını sağlar. Metin ölçeklemede ise Flutter metin widget'ları, işletim sisteminin yazı tipi büyütme ayarını font boyutunu belirlerken zaten dikkate alır; senin işin, düzeninde büyütülmüş fontun içeriği kaybetmeden sığacağı kadar boşluk bırakmaktır — sabit yükseklikli bir Container içine metni kilitlemek, bu ayarı devre dışı bırakmanın en hızlı yoludur.
Odak sırası ve klavye navigasyonu (web/masaüstü)
Flutter web ve masaüstü hedeflerinde klavyeyle gezinen bir kullanıcı için odak sırası, ekrandaki görsel sırayla aynı anlamı taşımalıdır — WCAG'ın "Focus Order" kriteri tam olarak bunu ister: sıralı gezinme mümkünse, odaklanabilir bileşenler anlamı ve işlerliği koruyan bir sırada odak almalıdır. Pratikte bu, Tab tuşuyla ilerlerken kullanıcının önce üstteki forma, sonra altındaki butona geçmesi demektir — sayfanın CSS'iyle görsel olarak yer değiştirmiş ama DOM/widget sırası değişmemiş bir bileşen, klavye kullanıcısını mantıksız bir sıçramaya zorlar.
Mobil-öncelikli bir ekip için bu bölüm genelde ihmal edilir, çünkü telefon ve tabletlerde klavye navigasyonu günlük test rutininde yer almaz — ama aynı Flutter kod tabanı web'e veya masaüstüne (Windows/macOS/Linux) derlendiğinde, fare kullanmayan ya da fizyolojik olarak fareyi kullanamayan bir kullanıcı için Tab/Shift+Tab ile gezinme tek erişim yoludur. Bir formu tasarlarken widget'ları ekranda görsel olarak nereye koyduğun ile widget ağacında hangi sırada tanımladığın genelde örtüşür, ama Stack, Positioned veya CSS benzeri mutlak konumlandırma kullanan karmaşık düzenlerde bu ikisi ayrışabilir; böyle bir durumda odak sırasını test etmenin en pratik yolu, mouse'u tamamen bırakıp sayfayı yalnızca Tab tuşuyla baştan sona dolaşmaktır — odağın nereye zıpladığını gözle takip ettiğinde, görsel sırayla mantıksal sıranın ayrıştığı yeri hemen fark edersin.
Otomatik test: accessibility guideline matcher'ları
Flutter'ın widget test framework'ü, erişilebilirlik kurallarını CI'da otomatik doğrulamak için hazır matcher'lar sunar — bunları yazmak, her release öncesi elle gezip kontrol etmekten çok daha ucuzdur:
- androidTapTargetGuideline: dokunulabilir düğümlerin Android için minimum 48×48 piksel olup olmadığını denetler.
- iOSTapTargetGuideline: dokunulabilir düğümlerin iOS için minimum 44×44 piksel olup olmadığını denetler.
- labeledTapTargetGuideline: dokunma/uzun-basma aksiyonu olan hedeflerin bir etiket taşıyıp taşımadığını denetler.
- textContrastGuideline: semantik düğümlerin minimum metin kontrastını karşılayıp karşılamadığını denetler; büyük metin (18pt ve üzeri normal) için önerilen kontrast 3:1'dir.
dart
1testWidgets('ana ekran dokunma hedefleri ve kontrast kurallarına uyar', (tester) async {2 final SemanticsHandle handle = tester.ensureSemantics();3 await tester.pumpWidget(const MyApp());4 5 await expectLater(tester, meetsGuideline(androidTapTargetGuideline));6 await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));7 await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));8 await expectLater(tester, meetsGuideline(textContrastGuideline));9 10 handle.dispose();11});Bu dört matcher, handle.dispose() çağrılana kadar açık kalan bir SemanticsHandle üzerinden çalışır — testin başında tester.ensureSemantics() çağırmazsan matcher'lar semantik ağaca erişemez ve test hata vererek düşer; docs'taki örnek bu yüzden final SemanticsHandle handle = tester.ensureSemantics(); ile başlayıp handle.dispose() ile biter.
Gerçek cihazda VoiceOver/TalkBack denetimi
Otomatik matcher'lar boyut ve kontrastı yakalar ama okuma sırasını, tonlamayı ve gerçek kullanıcı deneyimini yakalamaz — bunun için ekranı gerçek bir TalkBack (Android) veya VoiceOver (iOS) oturumunda, gözlerini kapatarak veya ekranı kapatarak dinlemen gerekir. Web tarafında aynı denetimi yaparken semantik ağacı gözle doğrulamak için profile veya release modunda -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true bayraklarıyla çalıştırabilirsin; bu, hangi widget'ın hangi etiketle ağaca girdiğini görsel olarak katman katman gösterir.
bash
1# Web'de, profile modunda semantik ağacı görsel katman olarak aç2flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=trueDenetim sırasında üç şeyi özellikle dinle: (1) her etkileşimli öğe okunduğunda amacı anlaşılıyor mu, (2) bir liste veya form içinde odak sırası mantıklı akıyor mu, (3) dinamik bir değişiklik (hata mesajı, sepet toplamı) sen dokunmadan duyuruluyor mu — üçüncüsü genelde liveRegion eksikliğinden aksar.
WCAG 2.2 ile eşleme ve teslim kontrol listesi
Flutter'ın Semantics API'si tek başına bir standart değil, WCAG 2.2'nin somut kriterlerini karşılamanın aracıdır. Teslim öncesi kontrol listesini bu eşlemeye göre kurmak, "erişilebilir mi?" sorusunu belirsiz bir histen somut bir kontrole çevirir:
WCAG 2.2 kriteri | Seviye | Flutter karşılığı |
|---|---|---|
4.1.2 Name, Role, Value | A | Semantics.label + rol bilgisi |
2.4.3 Focus Order | A | Odak sırası / FocusTraversalGroup |
2.5.8 Target Size (Minimum) | AA | 24×24 CSS px hedef boyutu |
1.4.3 Contrast (Minimum) | AA | textContrastGuideline (4.5:1 / 3:1) |
1.4.4 Resize Text | AA | OS font ölçekleme + esnek layout |
4.1.3 Status Messages | AA | Semantics(liveRegion: true) |
WCAG 2.2'nin AA zorunlu tabanı 24×24 CSS px'tir; yukarıdaki dokunma hedefi tablosundaki 44×44, W3C'nin tavsiye ettiği (2.5.5 Enhanced, AAA) ve Flutter dokümanının web için aktardığı daha yüksek hedeftir.
WCAG 4.1.2 "Name, Role, Value" (Level A), her UI bileşeninin adının ve rolünün programatik olarak belirlenebilmesini, kullanıcının ayarlayabildiği durum/özellik/değerlerin de programatik olarak ayarlanabilmesini ister — Flutter'da bunun karşılığı doğrudan Semantics.label ve widget'ın rol bilgisidir. WCAG 1.4.4 "Resize Text" (Level AA) ise metnin, yardımcı teknoloji olmadan, içerik veya işlev kaybı olmadan %200'e kadar büyütülebilmesini şart koşar; bu, yukarıdaki dokunma hedefi ve kontrast bölümünde anlattığım metin ölçekleme disiplininin standarttaki karşılığıdır. WCAG 4.1.3 "Status Messages" (Level AA), durum mesajlarının odak almadan programatik olarak AT'ye sunulabilmesini ister — Flutter'da bunu liveRegion karşılar.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Buraya kadar okuduğun için, bir sonraki release'inden önce ekranlarını hızlıca taramak için kullanabileceğin kısa bir teslim kontrol listesi hazırladım. Bunu bir PR şablonuna veya sprint'in "definition of done" listesine ekleyebilirsin; her madde, bu yazıda anlattığım bir kurala karşılık gelir ve beş dakikadan az sürer.
SSS
Flutter'da erişilebilirlik nasıl sağlanır?
Flutter'da erişilebilirlik, standart widget'ların otomatik ürettiği semantik ağacın Semantics, MergeSemantics ve ExcludeSemantics widget'larıyla düzenlenmesi, dokunma hedefi/kontrast/metin ölçekleme kurallarına uyulması ve bu davranışların hem otomatik guideline testleriyle hem de gerçek TalkBack/VoiceOver oturumlarıyla doğrulanmasıyla sağlanır. Tek bir adım değil, tasarım-kod-test üçlüsünde tutarlı bir disiplindir.
Semantics widget'ı ne işe yarar?
Semantics widget'ı, widget ağacını ekran okuyucuların ve diğer yardımcı teknolojilerin anlayacağı bir anlam açıklamasıyla etiketler; label, hint ve liveRegion gibi özellikleriyle bir bileşenin ne olduğunu, nasıl kullanılacağını ve dinamik değişip değişmediğini bildirir.
Flutter uygulaması VoiceOver ile nasıl test edilir?
Bir iOS cihazda Ayarlar > Erişilebilirlik'ten VoiceOver'ı açıp uygulamanın ana akışını ekranı görmeden, yalnızca sesli geri bildirimle baştan sona kullanarak test edersin; Flutter tarafında bu denetimi tamamlayan katman ise androidTapTargetGuideline, iOSTapTargetGuideline ve textContrastGuideline gibi otomatik testlerdir.
Flutter'da dokunma hedefi boyutu kaç piksel olmalı?
Android için minimum 48×48 dp, iOS için minimum 44×44 pt, W3C'nin web önerisinde ise 44×44 CSS piksel önerilir; Flutter'da bunu iOSTapTargetGuideline ve androidTapTargetGuideline test matcher'larıyla CI'da otomatik doğrulayabilirsin.
MergeSemantics ile ExcludeSemantics arasındaki fark nedir?
MergeSemantics, birden fazla alt widget'ın semantiğini tek bir anlamlı düğümde birleştirirken, ExcludeSemantics alt ağacın semantiğini tamamen erişilebilirlik ağacından düşürür; biri "birleştir", diğeri "gizle" der.
Güncelleme (Eylül 2026)
Bu yazının gövdesi 2026-06-23 tarihindeki Flutter davranışını anlatır; Flutter 3.47.0 sürümüyle birlikte erişilebilirlik tarafında birkaç somut değişiklik geldi. Android'de framework'ün semantik rollerinin bir kısmı artık native Android erişilebilirlik sınıflarına eşleniyor, bu da TalkBack'in bileşen türünü daha doğru duyurmasını sağlıyor. Aynı sürümde customSemanticsActions değiştiğinde ilgili SemanticsNode artık otomatik olarak "dirty" işaretleniyor — önceden özel bir aksiyonun güncellenmesi ekran okuyucuya yansımayabiliyordu, bu düzeltmeyle o risk kapandı.
iOS tarafında ise başlık seviyesine göre VoiceOver'ın "header" trait'i artık otomatik set ediliyor, bu da VoiceOver kullanıcılarının başlıklar arasında hızlı gezinmesini kolaylaştırıyor. Ayrıca semantik erişilebilirlik bloğu artık yalnız ekran okuyucu erişimini değil, klavye odaklanabilirliğini de engelliyor; bir modal açıkken arkadaki içeriğe hem ekran okuyucu hem klavye ile ulaşılamaz hale geldi. Test tarafında isSemantics/matchesSemantics eşleştiricilerine rol kontrolü eklendi ve çocuk uyuşmazlığı kontrolü sıkılaştırıldı, yani yanlış bir semantik rol artık testlerde daha güvenilir yakalanıyor. Kaynak: Flutter 3.47.0 release notes.
Sonuç
Flutter'da erişilebilirlik, tek seferlik bir denetim değil; Semantics ağacını doğru kurmak, MergeSemantics/ExcludeSemantics ile gereksiz gürültüyü temizlemek, dokunma hedefi ve kontrast eşiklerine uymak, bunu CI'da guideline matcher'larla otomatikleştirmek ve son adımda gerçek bir TalkBack/VoiceOver oturumuyla kulağınla doğrulamak demek. Bu beş adımı sprint rutinine yerleştirdiğinde erişilebilirlik, teslimden sonra eklenen bir "yama" olmaktan çıkar.
iOS tarafında aynı disiplinin platforma özgü karşılığını görmek istersen iOS erişilebilirlik rehberine bakabilirsin; genel WCAG çerçevesini ve platformlar arası kuralları mobil erişilebilirlik WCAG rehberinde daha geniş anlattım. Bu yazıdaki expectLater/meetsGuideline testlerini bir Flutter test paketine nasıl entegre edeceğini Flutter test rehberinde bulabilirsin. Erişilebilirlik testlerinin render performansıyla nasıl kesiştiğini merak ediyorsan Impeller render motoru yazısı iyi bir devam noktası. Genel performans disiplinini geniş tutmak istersen Flutter performans optimizasyonu rehberine da göz atabilirsin.
Kaynaklar
- Flutter — Assistive technologies — Semantics ağacı, TextSpan.locale, ensureSemantics ve ekran okuyucu eşlemesi.
- Flutter API — Semantics class — label/hint/liveRegion ve container parametresinin resmi tanımı.
- Flutter API — MergeSemantics class — alt widget semantiklerini birleştirme davranışı.
- Flutter API — ExcludeSemantics class — alt ağaç semantiğini düşürme davranışı ve Chip örneği.
- Flutter — UI design and styling for accessibility — dokunma hedefi boyutları, kontrast ve metin ölçekleme kuralları.
- Flutter — Accessibility testing — guideline matcher'lar ve
SemanticsHandleile test kurulumu. - WCAG 2.2 Quick Reference — W3C — 4.1.2, 2.4.3, 2.5.8, 1.4.3, 1.4.4, 4.1.3 kriterlerinin resmi metni.
- Flutter 3.47.0 Release Notes — Eylül 2026 güncelleme bölümündeki Semantics değişiklikleri.

