Přejít na obsah
7. března 2026|~20 min čtení

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.

iOSPWAReactTypeScriptMobile UX

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.

scroll-jump-sequence
1. iOS klávesnice se vysune
2. Safari posune dokument — input bar zůstane za klávesnicí
3. Uživatel nevidí kam píše
4. Zavře klávesnici, klepne znovu — někdy pomůže, někdy ne
5. UX působí rozbitě

Běžná řešení a proč nefungují

failed-solutions
Řešení Problém
window.scrollTo(0,0) při focusu
→ Scroll už proběhl — viditelný záblesk
overflow: hidden na body
→ Zabrání scrollu až po tom, co začal
position: fixed na root
→ Rozbije scroll obsahu (chat, feed)
Blokování touch eventů
→ Částečný fix, stále škubne
Všechna reagují PO scrollu. To je pozdě.

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.

safari-prediction
Uživatel klepne na textarea
Safari predikuje: "Bude textarea viditelná?"
NE — Safari posune dokument (scroll jump)
ANO — Safari nic neposune
Pokud přesvědčíme Safari že textarea JE viditelná → žádný skok

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.

Fáze 1Fáze 2Napište zprávu...Klávesnice(menší hodnota)přechodnáNapište zprávu...Input Accessory BarKlávesnice(finální hodnota)KB + IABDebounce 100ms čeká na Fázi 2

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í.

Bez pre-liftuS pre-liftemonClick / tap (standard)focus()Safari: textarea zakrytá!SCROLL JUMPscrollTo(0,0) — pozděonMouseDown (tap)PRE-LIFT input barufocus({ preventScroll: true })Safari: textarea viditelnáŽÁDNÝ SKOK
first-tap
První klepnutí (učící fáze)
1. Uživatel klepne na textarea nebo tlačítko
2. Systém nezná výšku → použije smart default
3. Focus proběhne
4. Safari může mírně scrollnout (minimální skok)
5. Fallback resetuje scroll na 0
6. iOS změří klávesnici → whitelist ověří → uloží
second-tap
Druhé a další klepnutí (perfektní)
1. Uživatel klepne na textarea nebo tlačítko
2. Systém zná výšku → okamžitý PRE-LIFT
3. Input bar se zvedne před focus eventem
4. Focus proběhne
5. Safari: "Je textarea viditelná?" → ANO
6. Žádný skok

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.

SmartDefaultsPre-LiftonMouseDownFocuspreventScrollViewportAPIWhitelistValidaceTransformtranslateYScrollLockDevice Detectiontouch + coarse pointerPŘED focusemPO focusu

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.

Input BarModáltranslateY(-100%)klávesnicePre-lift • Okamžitý • Scroll lockWhitelist • 100% posunPřihlášeníEmailHesloPřihlásit40%klávesniceTransition 0.3s • Bez scroll lockuBez whitelist • 40% posun

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í

Landscape mód
Whitelist funguje jen na portrait. Modály fungují v obou orientacích.
Externí klávesnice
Může vyvolat falešnou detekci virtuální klávesnice.
Split keyboard (iPad)
Nestandardní výška, systém ji ignoruje.
Klávesnice třetích stran
Nestandardní výšky mohou být zamítnuty whitelistem.

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

dont-do-this
Nechat onClick na prvcích v input baru — spustí se po focusu, pozdě
Předělat jen textarea — VŠECHNY prvky v baru musí být onMouseDown
Hardcoded výška bez měření — každá app má jiný layout
Globální body scroll prevention — rozbije modály
Výchozí výška 0 — skok při prvním klepnutí
Reset výšky při blur — ztráta naučené hodnoty
bottom místo transform — nespolehlivé na iOS
Testování jen v simulátoru — nereprodukuje problém
do-this
Předělat celý input bar z onClick na onMouseDown
Naměřit vlastní hodnoty na reálném zařízení
Body scroll lock jen podmíněně — jen pro input bar
Smart defaults podle zařízení a kontextu
Whitelist validace pro každý typ zařízení
useRef pro persistentní uložení výšky
transform: translateY pro pozicování
Testování na reálném iOS zařízení
Oddělený systém pro modály

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í

Smart defaults
Minimální skok při prvním klepnutí díky rozumnému odhadu výšky podle zařízení.
Pre-lift (onMouseDown)
Zabránění Safari scroll predikci zvednutím input baru před focus eventem.
Visual Viewport API
Měření skutečné výšky klávesnice s debounce pro Fázi 2.
Whitelist
Filtrování přechodných chybných hodnot — jen validní výšky se uloží.
Transform positioning
Spolehlivé GPU-akcelerované posouvání přes translateY.
Podmíněný scroll lock
Pojistka jen pro input bar. Modály mají vlastní systém bez scroll locku.

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 →