# Što ćete izraditi u ovom vodiču#
Ovo je praktičan vodič za pristupačnost u Flutteru fokusiran na tri područja koja uzrokuju najviše produkcijskih problema: Semantics, redoslijed fokusa i dinamički tekst. Dobit ćete i checklistu, copy-paste isječke te ponovljivu rutinu testiranja za TalkBack i VoiceOver.
Ako već održavate design system, uskladite svoja a11y pravila s odlukama o tipografiji i bojama. Ovo se dobro nadovezuje na naš post o temiranju i tipografiji u Flutteru: Temiranje u Flutter design systemu: Material 3, dinamičke boje, tipografija.
# Zašto je pristupačnost u Flutteru važna u 2026.#
Pristupačnost nije “nišni” zahtjev. WHO procjenjuje da više od 1,3 milijarde ljudi globalno živi s nekim oblikom invaliditeta, a mobitel im je često primarni kanal. U EU se zahtjevi za pristupačnost pooštravaju kroz rokove European Accessibility Acta, a mnogi timovi danas a11y probleme tretiraju kao blokere za release.
Iz perspektive proizvoda, poboljšanja pristupačnosti obično podižu ukupnu kvalitetu UX-a: jasnije oznake, bolji tap targeti, manje “odrezanih” layouta i predvidljivija navigacija. To znači manje ticketova prema podršci i višu konverziju na formama i checkout flowovima.
🎯 Ključna poruka: Ako vaš app radi na 200 posto skaliranja teksta uz screen reader i tipkovnicu, obično radi bolje za sve.
# Osnove pristupačnosti: kako Flutter izlaže vaš UI#
Flutter gradi semantics tree uz render tree. Screen readeri i alati za switch access čitaju semantics tree, ne vaš widget tree. Vaš je zadatak osigurati da semantičke uloge, oznake, vrijednosti i akcije točno opisuju UI i odgovaraju očekivanjima korisnika.
Tri praktične posljedice:
- 1Ako UI crtate ručno bez ugrađenih kontrola, lako je napraviti nešto što izgleda kao gumb, ali je nevidljivo asistivnoj tehnologiji.
- 2Redoslijed čitanja nije uvijek isti kao vizualni redoslijed, posebno kod stackova, overlaya i animacija.
- 3Dinamički tekst može razbiti layoute ako ne dizajnirate za skaliranje od prvog dana.
# Preduvjeti#
| Zahtjev | Verzija | Napomene |
|---|---|---|
| Flutter | 3.22+ | Radi i sa starijim verzijama, ali primjeri pretpostavljaju aktualni stable |
| Android uređaj ili emulator | Android 10+ | Potrebno za TalkBack i Switch Access provjere |
| iOS uređaj ili simulator | iOS 16+ | Potrebno za VoiceOver provjere |
| Osnovno znanje widget testiranja | — | Korisno za Semantics asercije; vidi link o testiranju niže |
Za strategiju testiranja, ovo se nadopunjuje s: Flutter testing pyramid: unit, widget, integration, golden.
# Semantics kako treba: oznake, uloge, vrijednosti i akcije#
Prvo preferirajte ugrađene kontrole#
Najlakši način da dobijete ispravne semantics postavke jest koristiti ElevatedButton, IconButton, Switch, CheckboxListTile, TextField, DropdownButton i ListTile umjesto da ih izmišljate ispočetka. Ugrađeni widgeti dolaze s ispravnim ulogama i accessibility akcijama za svaku platformu.
Kad baš morate napraviti custom kontrolu, omotajte je u Semantics i učinite je fokusabilnom i “actionable”.
Praktični Semantics obrasci (copy-paste)#
1) Gumbi samo s ikonom: uvijek dodajte tooltip ili semantičku oznaku
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Delete item',
onPressed: () => deleteItem(),
)Na mnogim platformama tooltip postaje pristupačna oznaka. Ako to preskočite, screen reader može izgovoriti samo “Button” ili besmislen naziv ikone.
2) Grupiranje sadržaja: spojite semantics za karticu u redu
Korisno je za list iteme kada želite jednu najavu umjesto čitanja svakog childa.
Semantics(
container: true,
label: 'Order 1832, delivered, total 48 euros',
hint: 'Double tap to view details',
button: true,
child: InkWell(
onTap: () => openOrder(),
child: const OrderCard(),
),
)Koristite container: true kada widget čini semantičku granicu.
3) Sakrijte dekorativne elemente
Ako je ikona isključivo dekorativna, izuzmite je kako ne bi zagušila najave.
Row(
children: const [
ExcludeSemantics(
child: Icon(Icons.star),
),
SizedBox(width: 8),
Text('Featured'),
],
)4) Live region najave za asinkrona ažuriranja
Za snackbare, greške i dinamičke status poruke, najavite promjene.
Semantics(
liveRegion: true,
child: Text(statusMessage),
)Koristite štedljivo. Pretjerivanje s live regionima stvara bučno i frustrirajuće iskustvo.
💡 Savjet: Kad lokalizirate, tretirajte semantičke oznake kao stringove vidljive korisniku. Ako lokalizirate UI tekst, ali ne i semantics, screen reader postaje iskustvo na miješanom jeziku.
Semantics checklist za interaktivne widgete#
Koristite ovo kao listu za build review.
| Stavka | Kako izgleda “dobro” | Brza provjera |
|---|---|---|
| Uloga | Button se čita kao button, switch kao switch | TalkBack kaže “Button” ili “Switch” kako treba |
| Oznaka | Opisuje akciju ili sadržaj | “Delete item” a ne “Trash” |
| Hint | Opcionalno, objašnjava ishod | “Double tap to view details” |
| Stanje | Selected, checked, expanded se najavljuje | Toggle najavi on ili off |
| Vrijednost | Slideri, progress indikatori izlažu vrijednosti | VoiceOver čita postotak |
| Tap target | Najmanje 48 x 48 logičkih piksela | Uključite pointer overlay ili izmjerite u devtools |
# Redoslijed fokusa i navigacija tipkovnicom#
Problemi s fokusom su broj jedan accessibility problem tipa “osjeća se pokvareno”, posebno u flowovima poput logina, checkouta i onboardinga.
Razumijevanje redoslijeda fokusa u Flutteru#
Flutterov redoslijed fokusa određuju traversal policy i redoslijed widgeta. Vizualni trikovi u layoutu poput Stack mogu stvoriti nesklad između onoga što korisnik vidi i onoga po čemu screen reader navigira.
Po potrebi koristite FocusTraversalGroup i ordered traversal.
Primjer: eksplicitan redoslijed fokusa za formu
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: const [
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: EmailField(),
),
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: PasswordField(),
),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: SubmitButton(),
),
],
),
)Ovo postaje kritično kad imate uvjetni UI, bannere ili inline validacijske widgete koji se pojave iznad polja.
Učinite custom widgete fokusabilnima i “actionable”#
Ako gradite custom gumb s GestureDetector, često nije fokusabilan tipkovnicom i možda ne izlaže ispravne semantics. Radije koristite InkWell ili TextButton. Ako baš morate ići custom, kombinirajte FocusableActionDetector sa semantics.
class AccessibleTileButton extends StatelessWidget {
const AccessibleTileButton({
super.key,
required this.label,
required this.onActivate,
});
final String label;
final VoidCallback onActivate;
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (intent) => onActivate(),
),
},
child: Semantics(
button: true,
label: label,
onTap: onActivate,
child: InkWell(
onTap: onActivate,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(label),
),
),
),
);
}
}Ovaj obrazac osigurava:
- Screen readeri vide gumb s oznakom.
- Korisnici tipkovnice mogu aktivirati Enterom ili Spaceom putem
ActivateIntent. - Korisnici dodira i dalje dobivaju
InkWellfeedback.
⚠️ Upozorenje: Ne oslanjajte se samo na semantics
onTapza podršku tipkovnici. Mnogi accessibility korisnici navigiraju hardverskim tipkovnicama, switch uređajima ili desktop platformama gdje fokus i aktivacija moraju raditi bez dodira.
Upravljanje fokusom za dijaloge, bottom sheetove i overlaye#
Čest problem: fokus ostane “iza” dijaloga ili prvi fokus završi na ikoni za zatvaranje umjesto na naslovu.
Praktična pravila:
- Pomaknite fokus na prvi smisleni element kad se modal otvori.
- Vratite fokus na element koji je otvorio modal kad se modal zatvori.
- Osigurajte da screen reader ne može navigirati na sadržaj u pozadini.
U Flutteru showDialog i showModalBottomSheet uglavnom upravljaju focus trapom, ali custom overlayi to mogu pokvariti. Koristite FocusScope i postavite inicijalni fokus.
final FocusNode confirmNode = FocusNode();
@override
void dispose() {
confirmNode.dispose();
super.dispose();
}
void openConfirmDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete item'),
content: const Text('This action cannot be undone.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
focusNode: confirmNode,
onPressed: () => confirmDelete(),
child: const Text('Delete'),
),
],
);
},
);
Future.microtask(() => confirmNode.requestFocus());
}# Standardi kontrasta i tap targeta koji se stvarno isporučuju#
Problemi s kontrastom su česti kad timovi usvoje dinamičke boje, gradijente ili “suptilan” UI. Za tekst, WCAG cilja omjer kontrasta 4.5:1 za normalan tekst i 3:1 za veliki tekst. Čak i ako ne radite formalnu certifikaciju, ovi pragovi su odlična baza za mobilne aplikacije.
Praktična checklist kontrasta#
| Područje | Minimalni cilj | Napomene |
|---|---|---|
| Tekst u tijelu | 4.5:1 | Najčešći fail je “mutni” sivi tekst |
| Veliki tekst | 3:1 | Veliko znači oko 18pt regular ili 14pt bold |
| Ikone koje prenose značenje | 3:1 | Dekorativne ikone mogu biti izuzete iz zahtjeva |
| Indikatori fokusa | Jasno vidljivi | Posebno za navigaciju tipkovnicom |
| Disabled tekst | I dalje čitljiv | Izbjegavajte ekstremno nizak kontrast za disabled stanja |
Ako koristite Material 3 dynamic color, validirajte kontrast i u light i u dark temi. Ovo je izravno vezano uz odluke design systema: Temiranje u Flutter design systemu: Material 3, dinamičke boje, tipografija.
ℹ️ Napomena: Problemi s kontrastom često se pojave samo na određenim OEM ekranima. Testirajte barem na jednom “nižem” Android uređaju gdje se svjetlina i kalibracija boja razlikuju od flagshipa.
Osigurajte minimalne tap targete#
Čak i savršeni semantics propadaju ako korisnici ne mogu pogoditi target. Koristite minimum 48 x 48 logičkih piksela za kontrole.
IconButton(
constraints: const BoxConstraints(minWidth: 48, minHeight: 48),
padding: EdgeInsets.zero,
icon: const Icon(Icons.close),
tooltip: 'Close',
onPressed: () => Navigator.pop(context),
)# Dinamički tekst i skaliranje teksta bez lomljenja layouta#
Skaliranje teksta je mjesto gdje se mnogi Flutter UI-ji raspadnu. Korisnici često povećaju font na 130 posto ili 200 posto. Praktičan cilj je: aplikacija ostaje upotrebljiva na 200 posto skaliranja teksta bez odrezanog teksta, bez preklapanja gumba i bez skrivenih polja u formama.
Pravila koja sprječavaju 90 posto problema#
- 1Izbjegavajte fiksne visine za containere s tekstom.
- 2Koristite
ExpandediFlexibleunutar rowova. - 3Za kritične informacije preferirajte wrap umjesto ellipsisa.
- 4Testirajte s “dugim” lokalima, posebno njemačkim i finskim.
- 5Izbjegavajte forsiranje text scale na 1.0 osim za brand-kritične assete gdje pristupačnost nije pogođena.
Primjer: row koji se skalira bez rezanja
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Account security',
style: TextStyle(fontWeight: FontWeight.w600),
),
SizedBox(height: 4),
Text(
'Enable two-factor authentication to protect your account.',
),
],
),
),
],
)Ne borite se protiv text scale factora#
Ponekad timovi “stisnu” skaliranje teksta kako bi sačuvali layout. To obično mijenja konzistentnost dizajna za isključivanje korisnika.
Ako stvarno morate ograničiti skaliranje za određenu komponentu, napravite to lokalno i namjerno, i zadržite kontrole upotrebljivima.
MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: const TextScaler.linear(1.1),
),
child: const PriceBadge(),
)Ovaj obrazac koristite samo kada je komponenta ornamentalna. Nemojte ga koristiti za forme, postavke, content ekrane ili onboarding copy.
Unos teksta: oznake, greške i helper tekst#
Text fieldovi su a11y “hotspot”. Najbolji obrazac je koristiti InputDecoration s label, hint i error, uz smislen error message.
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'name@company.com',
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.trim().isEmpty) return 'Email is required';
if (!value.contains('@')) return 'Enter a valid email address';
return null;
},
)Izbjegavajte generičke greške poput “Invalid input”. Korisnicima trebaju konkretne upute.
# Praktična checklist za pristupačnost u Flutteru (spremno za release)#
Koristite ovu checklistu tijekom PR reviewa i prije releasea.
Semantics#
- 1Svaki interaktivni element ima smislenu oznaku.
- 2Kontrole samo s ikonom imaju
tooltipili semantičku oznaku. - 3Dekorativne ikone i slike su izuzete iz semantics.
- 4Dinamička ažuriranja koja su bitna koriste
liveRegionštedljivo. - 5Custom widgeti izlažu ulogu i akcije, ne samo geste.
Fokus i navigacija#
- 1Redoslijed fokusa odgovara vizualnom redoslijedu na svakom ekranu.
- 2Nema focus trapova izvan modala, a modali ispravno “trappaju” fokus.
- 3Aktivacija tipkovnicom radi za custom kontrole putem
ActivateIntent. - 4Indikatori fokusa su vidljivi na desktop i web buildovima, ako je primjenjivo.
- 5Nakon zatvaranja dijaloga, fokus se vraća na kontrolu koja ga je otvorila.
Kontrast i targeti#
- 1Tekst zadovoljava 4.5:1 ili 3:1 za veliki tekst.
- 2Ikone koje prenose značenje imaju barem 3:1.
- 3Tap targeti su najmanje 48 x 48 logičkih piksela.
- 4Disabled stanja ostaju čitljiva i nisu jedini indikator stanja.
Skaliranje teksta#
- 1Nema odrezanih naslova, gumba ili labela u formama na 200 posto skaliranja.
- 2Rowovi koriste
Expandedili wrap umjesto fiksnih širina. - 3Kritični tekst ne ovisi o ellipsisu da bi prenio značenje.
- 4Layout ostaje upotrebljiv u oba smjera (portrait/landscape) gdje je podržano.
# Testiranje: TalkBack, VoiceOver i automatizirane provjere#
Ručni koraci testiranja za TalkBack (Android)#
- 1Uključite TalkBack u Android accessibility postavkama.
- 2Otvorite aplikaciju i prvo navigirajte bez touch explorationa, koristeći swipe desno i swipe lijevo.
- 3Provjerite:
- Svaka kontrola se najavljuje s oznakom i ulogom.
- Redoslijed ima smisla na svakom ekranu.
- Stanja togglea se najavljuju kao on ili off.
- 4Testirajte touch exploration povlačenjem prsta i slušanjem najava.
- 5Prođite kroz primarni flow, npr. sign up ili checkout, bez gašenja TalkBacka.
Ručni koraci testiranja za VoiceOver (iOS)#
- 1Uključite VoiceOver u Settings.
- 2Navigirajte swipe desno i swipe lijevo, zatim rotor gdje je relevantno.
- 3Provjerite:
- Gumbi najavljuju oznaku fokusiranu na akciju.
- Headings ili grupiranja su smisleni na ekranima s puno sadržaja.
- Alerti i greške se najavljuju, a ne samo vizualno prikazuju.
Automatizirano testiranje: Semantics u widget testovima#
Možete provjeriti da oznake postoje i da se nodeovi mogu tapnuti. To ne zamjenjuje testiranje na uređaju, ali brzo hvata regresije.
testWidgets('Delete button exposes accessible label', (tester) async {
await tester.pumpWidget(const MyApp());
final semantics = tester.getSemantics(find.byIcon(Icons.delete));
expect(semantics.label, 'Delete item');
expect(semantics.hasAction(SemanticsAction.tap), true);
});Za kompletnu strategiju testiranja, kombinirajte ove provjere s integration testovima i golden testovima: Flutter testing pyramid: unit, widget, integration, golden.
💡 Savjet: Dodajte jedan widget test fokusiran na semantics za svaki kritični ekran. Ovi testovi su jeftini za održavanje i sprječavaju “label drift” kad se UI tekst promijeni.
# Česte zamke kod custom widgeta#
1) GestureDetector kao gumb#
GestureDetector bez semantics često stvara nevidljive kontrole. Koristite InkWell ili TextButton. Ako morate koristiti GestureDetector, dodajte Semantics(button: true, onTap: ...) i podršku za aktivaciju tipkovnicom.
2) Stackovi i overlayi kvare redoslijed čitanja#
Stack može uzrokovati da screen reader prolazi redoslijedom kreiranja widgeta, što možda ne odgovara vizualnom poretku. Popravite s:
- eksplicitnim traversal redoslijedom u kritičnim dijelovima
- pažljivim redoslijedom widgeta
- grupiranjem pomoću
Semantics(container: true)
3) Ponovno korištenje iste semantičke oznake za više stavki#
Liste koje stalno najavljuju “Button” ili “Item” su neupotrebljive. Uključite jedinstvene informacije poput naziva stavke, statusa i cijene.
Loš obrazac: “Open item” za svaki red.
Dobar obrazac: “Open invoice 1832, due tomorrow, 48 euros”.
4) Skaliranje teksta otkriva skrivene bugove layouta#
Hardkodirane visine, baseline alignment hackovi i pixel-perfect constraintovi često režu tekst na 130 posto i više. Popravite dopuštanjem wrapa i izbjegavanjem fiksnih visina oko teksta.
5) Oslanjanje samo na boju#
Error stanja koja samo pocrvene padaju kod daltonista i teže su uočljiva za screen readere. Dodajte ikone, helper tekst i semantičke najave.
Ako vaš tim radi i web UI-je, mentalni model se preklapa s ARIA-om i upravljanjem fokusom tipkovnicom. Ova poveznica je korisna za zajedničke standarde: React checklist za pristupačnost: ARIA i fokus tipkovnice.
# Ključne poruke#
- Koristite ugrađene Material i Cupertino widgete kad god možete i dodajte
tooltipza akcije samo s ikonom kako biste garantirali smislene oznake. - Kontrolirajte redoslijed fokusa s
FocusTraversalGroupi ordered traversal na složenim ekranima, posebno formama i stacked layoutima. - Učinite custom kontrole aktivirljivima tipkovnicom s
FocusableActionDetectori izložite ulogu i akcije putemSemantics. - Dizajnirajte za 200 posto skaliranja teksta izbjegavanjem fiksnih visina, korištenjem
Expandedi dopuštanjem wrapa na kritičnom sadržaju. - Testirajte svaki release end-to-end s TalkBackom i VoiceOverom te dodajte Semantics asercije u widget testove kako biste spriječili regresije.
# Zaključak#
Pristupačnost u Flutteru nije jednokratni zadatak. To je skup defaulta, ponovljivih obrazaca i testnih navika koje sprječavaju regresije kako aplikacija raste.
Ako želite brz, produkcijski “a11y pass” za svoj Flutter app, Samioda vam može pomoći auditirati semantics i redoslijed fokusa, popraviti probleme sa skaliranjem teksta i kontrastom te dodati automatiziranu pokrivenost da sve ostane stabilno. Javite se i predložit ćemo konkretnu checklistu i plan implementacije za vaš codebase.
FAQ
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Mobilni razvoj
Sve →Optimizacija Flutter cold starta: brže pokretanje, bolji splash, manje jank frameova
Praktični vodič za 2026. za optimizaciju Flutter cold starta: kako profilirati vrijeme pokretanja, smanjiti tešku inicijalizaciju, izbjeći sinkroni I/O, ukrotiti trošak pokretanja plugina i isporučiti glađi prvi frame uz bolji splash i sekvenciranje deep linkova.
Flutter analitika i atribucija za startupove: Firebase vs Amplitude vs AppsFlyer (što pratiti i zašto)
Usporedba Firebase Analyticsa, Amplitudea i AppsFlyera iz perspektive startupa za Flutter analitiku i atribuciju — trud oko postavljanja, trošak, GDPR/privatnost kompromisi, modeliranje događaja te MVP-spremna taksonomija praćenja s planom uvođenja.
Flutter + Supabase Upload Datoteka: Sigurna pohrana, potpisani URL-ovi, promjena veličine slika i kontrola pristupa
Praktičan vodič za 2026. za Flutter Supabase tokove uploada datoteka: upload iz kamere i galerije, pozadinski retry, sigurne Storage politike, potpisani URL-ovi za preuzimanje, promjena veličine slika i error handling na razini produkcije.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Strategija testiranja u Flutteru: unit, widget, integracijski i golden testovi za brz i pouzdan CI (2026)
Praktična strategija testiranja u Flutteru temeljena na piramidi testiranja: kada koristiti unit, widget, integracijske i golden testove, kako smanjiti flakiness i kako sve pokretati brzo u CI-u.
Flutter navigacija s go_router: duboke poveznice, auth guardovi, ugniježđene rute i podrška za web
Vodič spreman za produkciju za Flutter go_router deep links, uključujući auth preusmjeravanja, ShellRoute layout, ugniježđenu navigaciju, stanje vođeno URL-om i testiranje deep linkova za iOS, Android i web.
Flutter zadaci u pozadini: raspoređivanje, pouzdanost i ograničenja platformi (iOS + Android) u 2026.
Praktičan vodič za raspoređivanje Flutter zadataka u pozadini: ograničenja platformi na iOS-u i Androidu, kompromisi pouzdanosti te kada koristiti workmanager, background_fetch ili izvorni kod za sinkronizaciju, obavijesti i raspoređivanje koje čuva bateriju.