Implementing a Keyboard‑Attached Input Using VisualViewport Events
This article explains how to create an input element that sticks to the on‑screen keyboard by listening to visualViewport resize/scroll and focus events, calculating the keyboard’s top position, and dynamically adjusting the input’s CSS transform, with full TypeScript code examples.
Demo and Effect
The article starts by providing a live demo and a preview GIF showing the input element moving together with the virtual keyboard.
Implementation Principle
To attach an input to the keyboard, three steps are required: (1) listen for keyboard height changes, (2) obtain the distance from the keyboard top to the viewport top, and (3) set the input’s position accordingly.
Step 1: Listening to Keyboard Height Changes
Different browsers emit different events when the keyboard shows or hides. iOS and some Android browsers fire a sequence of visualViewport resize → focusin → visualViewport scroll on show and visualViewport resize → focusout → visualViewport scroll on hide. Other Android browsers emit continuous window resize events.
Based on this, the following listeners are added:
if (window.visualViewport) {
window.visualViewport?.addEventListener("resize", listener);
window.visualViewport?.addEventListener("scroll", listener);
} else {
window.addEventListener("resize", listener);
}
window.addEventListener("focusin", listener);
window.addEventListener("focusout", listener);These events allow the script to detect when the viewport height changes due to the keyboard.
Determining Keyboard Show/Hide State
The article suggests a rule: if any of the above events fire and the viewport height decreases by more than 200 px, the keyboard is considered shown; if the height increases and the change is less than 200 px, it is considered hidden.
// Get current viewport height
const height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
// Height delta compared to previous measurement
const diffHeight = height - lastWinHeight;
// Keyboard height = default screen height - current viewport height
const keyboardHeight = DEFAULT_HEIGHT - height;
if (diffHeight < 0 && keyboardHeight > 200) {
onKeyboardShow();
} else if (diffHeight > 0) {
onKeyboardHide();
}A 200 ms debounce is added to avoid rapid toggling caused by viewport quirks.
let canChangeStatus = true;
function onKeyboardShow({ height, top }) {
if (canChangeStatus) {
canChangeStatus = false;
setTimeout(() => {
callback();
canChangeStatus = true;
}, 200);
}
}Step 2: Calculating Keyboard Top Position
The keyboard top relative to the viewport top equals the current viewport height plus the viewport’s scroll offset:
// Get current viewport height
const height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
// Get viewport scroll offset
const viewportScrollTop = window.visualViewport?.pageTop || 0;
// Keyboard top position
const keyboardTop = height + viewportScrollTop;Step 3: Setting the Input Position
The input is styled with absolute positioning and full width. Its vertical offset is calculated as the keyboard top minus the input’s own height, applied via a CSS translateY transform.
input {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 50px;
transition: all .3s;
}The observer updates the transform on each keyboard position change:
// input has position absolute, top 0
keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});Full Implementation Code
import EventEmitter from "eventemitter3";
// Default screen height
const DEFAULT_HEIGHT = window.innerHeight;
const MIN_KEYBOARD_HEIGHT = 200;
// Keyboard events
export enum KeyboardEvent {
Show = "Show",
Hide = "Hide",
PositionChange = "PositionChange",
}
interface KeyboardInfo {
height: number;
top: number;
}
class KeyboardObserver extends EventEmitter {
inited = false;
lastWinHeight = DEFAULT_HEIGHT;
canChangeStatus = true;
_unbind = () => {};
// Initialize keyboard handling
init() {
if (this.inited) return;
const listener = () => this.adjustPos();
if (window.visualViewport) {
window.visualViewport?.addEventListener("resize", listener);
window.visualViewport?.addEventListener("scroll", listener);
} else {
window.addEventListener("resize", listener);
}
window.addEventListener("focusin", listener);
window.addEventListener("focusout", listener);
this._unbind = () => {
if (window.visualViewport) {
window.visualViewport?.removeEventListener("resize", listener);
window.visualViewport?.removeEventListener("scroll", listener);
} else {
window.removeEventListener("resize", listener);
}
window.removeEventListener("focusin", listener);
window.removeEventListener("focusout", listener);
};
this.inited = true;
}
// Unbind events
unbind() {
this._unbind();
this.inited = false;
}
// Adjust keyboard position
adjustPos() {
const height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
const keyboardHeight = DEFAULT_HEIGHT - height;
const top = height + (window.visualViewport?.pageTop || 0);
this.emit(KeyboardEvent.PositionChange, { top });
const diffHeight = height - this.lastWinHeight;
this.lastWinHeight = height;
if (diffHeight < 0 && keyboardHeight > MIN_KEYBOARD_HEIGHT) {
this.onKeyboardShow({ height: keyboardHeight, top });
} else if (diffHeight > 0) {
this.onKeyboardHide({ height: keyboardHeight, top });
}
}
onKeyboardShow({ height, top }: KeyboardInfo) {
if (this.canChangeStatus) {
this.emit(KeyboardEvent.Show, { height, top });
this.canChangeStatus = false;
this.setStatus();
}
}
onKeyboardHide({ height, top }: KeyboardInfo) {
if (this.canChangeStatus) {
this.emit(KeyboardEvent.Hide, { height, top });
this.canChangeStatus = false;
this.setStatus();
}
}
setStatus() {
const timer = setTimeout(() => {
clearTimeout(timer);
this.canChangeStatus = true;
}, 300);
}
}
const keyboardObserver = new KeyboardObserver();
export default keyboardObserver;Usage example:
keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});The article concludes that the implementation is straightforward and encourages readers to explore the complete code.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.