Frontend Development 9 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing a Keyboard‑Attached Input Using VisualViewport Events

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.

frontendJavaScriptkeyboardEvent HandlinginputvisualViewport
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.