Last active
November 26, 2025 20:20
-
-
Save Lukas1h/92432ce6d4e73165e36401022b289f1b to your computer and use it in GitHub Desktop.
iOS Keyboard Handeling in SvelteKit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script lang="ts"> | |
| import { store } from '$lib/store/store.svelte'; | |
| import { isIOS } from '$lib/utils'; | |
| // Keyboard and scroll logic for iOS. | |
| // Does the following: | |
| // - Uses heuristics to guess if the keyboard's open and it's height. | |
| // - Prevents scrolling below the keyboard. | |
| // - Closes the keyboard when scrolling up. | |
| onMount(() => { | |
| if (!isIOS()) return; | |
| if (window.visualViewport) { | |
| let height = window.visualViewport?.height || 0; | |
| let keyboardOpenedDate = 0; | |
| let isIntervalRunning = false; | |
| // Watch for viewport size changes. | |
| window.visualViewport.addEventListener('resize', (e) => { | |
| const newHeight = window.visualViewport?.height || 0; | |
| const heightDifference = height - newHeight; | |
| const textareaFocused = | |
| document.activeElement?.tagName === 'INPUT' || | |
| document.activeElement?.tagName === 'TEXTAREA'; | |
| if (store.keyboardHeight == undefined && textareaFocused) { | |
| // If the viewport height changed while a textarea was | |
| // focused for the first time, then the keyboard was probably opened. | |
| store.keyboardHeight = heightDifference; | |
| } else if ( | |
| store.keyboardHeight != undefined && | |
| heightDifference < store.keyboardHeight && | |
| textareaFocused | |
| ) { | |
| // Sometimes the viewport height will change to ramdon things | |
| // when scrolling, but the difference is always greater then the keyboard's height. | |
| // This updates the keyboard height if it were to ever change for some reason. | |
| // Probably don't need this, but it's here for robustness. | |
| store.keyboardHeight = heightDifference; | |
| } | |
| // If the viewport just shrank and a textarea is focused, | |
| // then the keyboard was probably opened. | |
| if ( | |
| newHeight < height && | |
| textareaFocused && | |
| !store.isKeyboardOpen && | |
| window.scrollY <= (store.keyboardHeight || 0) | |
| ) { | |
| keyboardOpenedDate = Date.now(); | |
| store.isKeyboardOpen = true; | |
| console.log('====. keyboard opened!!'); | |
| } | |
| if ( | |
| newHeight > height && | |
| store.isKeyboardOpen && | |
| window.scrollY <= (store.keyboardHeight || 0) | |
| ) { | |
| store.isKeyboardOpen = false; | |
| console.log('====. keyboard closed!!'); | |
| } | |
| }); | |
| // Watch for scrolling. | |
| window.addEventListener('scroll', (e) => { | |
| console.log('scrolled'); | |
| // Window resizing also triggers scrolling, | |
| // so wait 100ms until after the resizing when | |
| // the keyboard is hopefully done animating. | |
| // If the keyboard is open, and the window is scrolled | |
| // past the input into the blank area, then scroll back up | |
| // so the blank area isn't taking up space. | |
| if (store.keyboardHeight && store.isKeyboardOpen && window.scrollY > store.keyboardHeight) { | |
| window.scrollTo(0, store.keyboardHeight); | |
| } | |
| if (!isIntervalRunning) { | |
| isIntervalRunning = true; | |
| // When the keyboard is opened, it resized the viewport | |
| // and also trigers the scroll handeler, so we need to wait | |
| // 500ms until after the keyboard has finished opening to | |
| // run this check. | |
| setTimeout(() => { | |
| isIntervalRunning = false; | |
| // If the keyboard is open, and the window is scrolled | |
| // up, then close the keyboard. | |
| if ( | |
| store.keyboardHeight && | |
| store.isKeyboardOpen && | |
| window.scrollY < store.keyboardHeight | |
| ) { | |
| (document.activeElement as HTMLInputElement)?.blur(); | |
| } | |
| }, 500); | |
| } | |
| }); | |
| } | |
| }); | |
| </script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class Store { | |
| keyboardHeight: number | undefined = $state(undefined) | |
| isKeyboardOpen = $state(false) | |
| } | |
| export const store = new Store() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export function isIOS() { | |
| return ( | |
| /iPad|iPhone|iPod/.test(navigator.userAgent) || | |
| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment