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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
