iOS PWA Keyboard Scroll Jump Fix
Kompletní řešení scroll skoku při otevření klávesnice v iOS PWA. Proč to skáče, proč běžná řešení nefungují, a jak to opravit na úrovni nativní aplikace.
01Problém
Když uživatel klepne na textové pole v PWA na iOS, klávesnice se vysune a celá stránka sebou škubne. Často ale problém není jen vizuální — klávesnice se zvedne dřív než input bar, který zůstane schovaný za klávesnicí. Uživatel pak musí zavřít klávesnici a klepnout znovu. Někdy se input bar zvedne, někdy ne. Nespolehlivé a frustrující.
Týká se prakticky každé PWA s input barem fixovaným na spodní hraně — včetně ChatGPT, Claude.ai, Notion nebo Discord.
Běžná řešení a proč nefungují
02Jak Safari rozhoduje o scrollu
Safari má prediktivní logiku. Ještě před tím, než se klávesnice plně zobrazí, Safari vyhodnotí jestli bude textarea viditelná. Pokud ne — posune dokument. Klíčový poznatek: Safari scrolluje PŘED otevřením klávesnice.
Dvoufázové měření klávesnice
iOS měří výšku klávesnice ve dvou fázích. Fáze 1 — samotná klávesnice (menší hodnota). Fáze 2 — klávesnice + Input Accessory Bar (finální hodnota). IAB je lišta nad klávesnicí, kterou iOS 18+ zobrazuje jako samostatný UI element.
Jak naměřit vlastní hodnoty
- Otevřete aplikaci na reálném iOS zařízení (ne simulátor)
- Přidejte console.log(window.innerHeight - window.visualViewport.height) při resize eventu
- Klepněte na textarea a sledujte konzoli
- Zapište si stabilní hodnoty pro textovou a emoji klávesnici
- Opakujte pro: iPhone PWA, iPhone prohlížeč, iPad
- Tyto hodnoty použijte ve whitelistu
03Řešení: Conditional Pre-Lift
Princip je jednoduchý — nepotlačovat scroll poté co proběhne, ale zabránit mu, aby vůbec nastal. Input bar zvedneme PŘED focus eventem. Safari pak vidí textarea jako viditelnou a nescrolluje. Díky tomu se input bar vždy spolehlivě zvedne nad klávesnici — žádné opakované klepání, žádné schování za klávesnicí.
04Implementace
Implementace má 7 kroků + detekci zařízení. Každý krok řeší konkrétní problém — smart defaults, pre-lift timing, měření klávesnice, filtrování chybných hodnot, pozicování a fallback.
1. Smart defaults podle zařízení
Než uživatel poprvé otevře klávesnici, systém potřebuje rozumný odhad výšky. Místo 0 použijeme hodnoty naměřené na vašem layoutu:
2. Trvalý stav přes useRef
Výšku klávesnice ukládáme do useRef, ne do useState — nepotřebujeme re-render při každé změně, a hodnota přežije překreslení komponenty:
3. Pre-lift na onMouseDown
Standardně se v Reactu používá onClick pro interakce. Pro pre-lift to nestačí — onClick se spustí PO focus eventu, kdy už je pozdě. onMouseDown se spustí PŘED focusem. To je náš okamžik pro pre-lift.
Důležité: celý input bar musíte předělat z onClick na onMouseDown. Všechny interaktivní prvky — textarea, tlačítko odeslat, příloha, emoji picker, všechno. Pokud cokoliv zůstane na onClick, pre-lift se nespustí a skok se vrátí.
4. Detekce klávesnice přes Visual Viewport API
Klávesnice zmenšuje vizuální viewport. Rozdíl mezi původní výškou okna a aktuální výškou viewportu = výška klávesnice. Debounce 100ms čeká na Fázi 2 s IAB.
5. Whitelist — filtrování chybných hodnot
Během přechodů klávesnice iOS hlásí přechodné hodnoty které neodpovídají reálné výšce. Tyto chaos values by znehodnotily uloženou výšku. Přijímáme jen hodnoty odpovídající naměřeným výškám:
6. Pozicování přes transform
Na iOS je transform: translateY spolehlivější než bottom pro posouvání fixed elementů. willChange: transform zajistí GPU akceleraci. Žádná transition — zvednutí musí být okamžité.
7. Body scroll prevention — podmíněná pojistka
Pojistka pro případ selhání pre-liftu. Důležité: nesmí být globální. Globální blokování scrollu rozbije inputy v modálech a formulářích — klávesnice je zakryje. Zapínat jen když je aktivní input bar:
8. Detekce zařízení
Feature detection místo user-agent sniffingu:
05Klávesnice v modálech
Modály s inputy (login, profil, nastavení) potřebují jiný přístup než fixní input bar. Nepotřebují pre-lift, stačí plynulý posun. A hlavně — body scroll nesmí být zablokovaný, jinak klávesnice zakryje input.
06Edge cases
Ošetřené
- První klepnutí — smart defaults + fallback scroll prevention
- Přepínání text/emoji — whitelist obsahuje obě výšky
- Otočení zařízení — reset měření při změně viewportu
- Přechod na pozadí — iOS zavře klávesnici bez blur eventu
- Přechodné hodnoty — whitelist je ignoruje
- PWA vs prohlížeč — různé výšky, automatická detekce
- iPad vs iPhone — iPad nemá IAB, automatická detekce
- Inputy mimo input bar — body scroll lock je podmíněný
Známá omezení
Pro většinu uživatelů (95%+) systém funguje spolehlivě. Omezení se týkají okrajových scénářů.
07Časté chyby při implementaci
08Android
Na Androidu je situace jednodušší. Chrome 94+ podporuje VirtualKeyboard API, které přímo reportuje výšku klávesnice bez potřeby workaroundů. Celý pre-lift systém je potřeba jen na iOS.
09Shrnutí
Výsledek
Řešení eliminuje ~95 % scroll skoků. První klepnutí může mít minimální skok (smart defaults ho zmírňují), každé další klepnutí je plynulé — na úrovni nativní aplikace.
Testováno na
- iOS 18.7 — iPhone s Face ID (PWA i prohlížeč)
- iOS 26.x+ — nejnovější beta, ověřeno v PWA
Starší iPhony s Touch ID (SE, 7) mají jiný viewport — home button místo gesture baru mění rozložení. Whitelist hodnoty z Face ID zařízení na nich nebudou sedět. Na těchto zařízeních je potřeba provést vlastní měření a nastavit odpovídající whitelist. Zatím neotestováno.
Vždy testujte na reálném zařízení — simulátor problém nereprodukuje. Whitelist hodnoty se liší mezi PWA a prohlížečem, mezi Face ID a Touch ID zařízeními, a mezi portrait a landscape orientací.
Kompletní dokumentace a kód jsou dostupné na GitHubu — anglická i česká verze.
GitHub: ios-pwa-keyboard-fix →