HTML text events: TextEvent, textInput, and what to use instead

TextEvent is an old browser interface behind the textInput event, and MDN marks it deprecated. For text fields, listen to beforeinput and input instead.

In the web platform, TextEvent is a legacy interface that reports changes to text elements. It is the object behind the textInput event, and MDN marks it deprecated.

For the same job in today's HTML, listen for beforeinput (about to change) and input (has changed) on the text field.

Type in the box below to see every text event a field sends, in order. The orange line is the legacy textInput.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Text events in order</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
  input { width: 100%; box-sizing: border-box; font-size: 16px; padding: 9px 11px; border: 1px solid #c9cdd4; border-radius: 8px; }
  #log { margin: 12px 0 0; padding: 0; list-style: none; height: 250px; overflow: auto; background: #fff; border: 1px solid #e1e4ea; border-radius: 8px; font: 13px/1.5 ui-monospace, Consolas, monospace; }
  #log li { padding: 2px 10px; border-bottom: 1px solid #f0f1f4; }
  #log li.sep { background: #f4f5f7; color: #6b7280; font-size: 12px; }
  .name { display: inline-block; min-width: 92px; font-weight: 700; }
  .keydown .name { color: #6b7280; }
  .beforeinput .name { color: #1d4ed8; }
  .textInput .name { color: #9a3412; }
  .input .name { color: #0f5132; }
  .change .name { color: #7c3aed; }
  #note { margin: 8px 0 0; font-size: 12.5px; color: #5b6270; }
</style>
</head>
<body>
<label for="f">Type here, then press Tab</label>
<input id="f" type="text" autocomplete="off" placeholder="Type a letter, paste, press Backspace">
<ul id="log"></ul>
<p id="note">Orange = legacy textInput. If it never appears, your browser does not send it.</p>

<script>
  const field = document.getElementById('f');
  const log = document.getElementById('log');

  function add(type, detail) {
    const li = document.createElement('li');
    li.className = type;
    li.innerHTML = '<span class="name">' + type + '</span>' + detail;
    log.prepend(li);                       // newest on top
    while (log.children.length > 40) log.lastChild.remove();
  }

  field.addEventListener('keydown', (e) => add('keydown', 'key: ' + JSON.stringify(e.key)));

  field.addEventListener('beforeinput', (e) =>
    add('beforeinput', 'inputType: ' + e.inputType + ', data: ' + JSON.stringify(e.data)));

  // Legacy event: the name is case-sensitive, "textInput" with a capital I.
  field.addEventListener('textInput', (e) =>
    add('textInput', 'data: ' + JSON.stringify(e.data)));

  field.addEventListener('input', (e) =>
    add('input', 'value is now ' + JSON.stringify(field.value)));

  field.addEventListener('change', () => {
    add('change', 'committed ' + JSON.stringify(field.value));
  });
</script>
</body>
</html>
Type a letter, paste, or press Backspace. Newest events are on top. Press Tab to see change.

The log shows what one field sends for one keystroke. Details can differ between browsers, so run it in the ones you care about.

What TextEvent and textInput are

MDN describes TextEvent as "a legacy UI event interface for reporting changes to text UI elements". It has one property, data, and one method, initTextEvent().

The UI Events specification lists textInput among its obsolete event types. It says those should only be implemented by browsers that need compatibility with legacy software.

The legacy textInput event next to the two events that replace it.
The legacy textInput event next to the two events that replace it.

MDN's compatibility data lists TextEvent in Chrome 1, Safari 3, Edge 12 and Firefox 129. Support does not change the advice: the page is marked deprecated, and the events that replaced it do more.

The events to use instead

A text field sends a short chain of events for each change. Each one answers a different question.

The four events that matter for text fields, in the order a typed character produces them.
The four events that matter for text fields, in the order a typed character produces them.
Event Fires Use it to
keydown A key goes down Handle shortcuts and special keys
beforeinput The value is about to change Inspect or block the change
input The value has changed Read the new value, update a counter
change The value is committed, such as on blur Save once, after editing ends
keypress Deprecated key event Nothing: MDN says to use beforeinput or keydown

If you only remember one: use input to follow what the user typed. addEventListener covers how to attach any of these.

Read what changed: data and inputType

Both beforeinput and input hand you an InputEvent. It has three useful properties, and TextEvent has none of them except data.

  • data – the characters being inserted.
  • inputType – the kind of change, such as insertText, insertFromPaste or deleteContentBackward.
  • isComposing – true between compositionstart and compositionend, which is how an input method editor builds a character.

MDN says data can be empty when a change inserts no text, such as deleting. In a Chromium test, Backspace gave null in beforeinput. Check it before you use it:

field.addEventListener('beforeinput', (e) => {
  if (e.data && e.inputType === 'insertText') {
    console.log('typed', e.data);
  }
});

The log also shows that Backspace sends beforeinput and input but no textInput in that test. That gap is one more reason not to build on the legacy event.

Block a character before it lands

beforeinput runs before the browser changes the value. Calling preventDefault() there stops the change. The left box below refuses letters this way. The right box lets everything in, then removes letters in input.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Block letters: beforeinput vs input</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  @media (max-width: 460px) { .grid { grid-template-columns: 1fr; } }
  .panel { background: #fff; border: 1px solid #e1e4ea; border-radius: 10px; padding: 12px; }
  h3 { margin: 0 0 4px; font-size: 14px; }
  .panel p { margin: 0 0 8px; font-size: 12.5px; color: #5b6270; }
  input { width: 100%; box-sizing: border-box; font-size: 16px; padding: 9px 11px; border: 1px solid #c9cdd4; border-radius: 8px; }
  .out { margin-top: 8px; font: 12.5px ui-monospace, Consolas, monospace; color: #374151; min-height: 18px; }
  .a h3 { color: #1d4ed8; } .b h3 { color: #0f5132; }
</style>
</head>
<body>
<div class="grid">
  <div class="panel a">
    <h3>Cancel in beforeinput</h3>
    <p>Typed letters never reach the box.</p>
    <input id="a" inputmode="numeric" autocomplete="off" placeholder="digits only">
    <div class="out" id="outA"></div>
  </div>
  <div class="panel b">
    <h3>Clean up in input</h3>
    <p>Anything goes in, then letters are removed. Paste works too.</p>
    <input id="b" inputmode="numeric" autocomplete="off" placeholder="digits only">
    <div class="out" id="outB"></div>
  </div>
</div>

<script>
  const a = document.getElementById('a');
  const b = document.getElementById('b');

  // A: stop a non-digit before it is inserted
  a.addEventListener('beforeinput', (e) => {
    if (e.inputType === 'insertText' && e.data && /\D/.test(e.data)) {
      e.preventDefault();
      document.getElementById('outA').textContent = 'blocked ' + JSON.stringify(e.data);
    }
  });

  // B: let anything in, then strip non-digits (also catches paste)
  b.addEventListener('input', () => {
    const clean = b.value.replace(/\D/g, '');
    if (clean !== b.value) {
      b.value = clean;
      document.getElementById('outB').textContent = 'removed non-digits';
    }
  });
</script>
</body>
</html>
Left: cancel in beforeinput. Right: clean up in input. Paste a mix of letters and digits into both.

Paste into the left box. The letters get through, because the guard only checks insertText, and a paste has a different inputType.

MDN also warns that not every change is cancelable. Autocomplete, spell-check corrections, password-manager autofill and IME may skip it.

beforeinput alone can miss changes; adding an input listener catches the rest.
beforeinput alone can miss changes; adding an input listener catches the rest.

The reliable pattern is both. Cancel the easy cases in beforeinput, then make the value correct in input:

field.addEventListener('input', () => {
  field.value = field.value.replace(/\D/g, '');
});

For a number field, check the built-in options first. Text input in HTML covers type, inputmode and pattern.

A finished example: a live counter

The same input listener drives a character counter. It trims pasted overflow, fills a bar, and shows the inputType of the last change.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Live counter with a limit</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .card { background: #fff; border: 1px solid #e1e4ea; border-radius: 12px; padding: 14px; box-shadow: 0 4px 14px rgba(0,0,0,.06); }
  label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
  textarea { width: 100%; box-sizing: border-box; height: 110px; resize: vertical; font: 16px/1.4 system-ui, sans-serif; padding: 9px 11px; border: 1px solid #c9cdd4; border-radius: 8px; }
  .bar { height: 6px; background: #e5e7eb; border-radius: 3px; margin: 10px 0 6px; overflow: hidden; }
  .fill { height: 100%; width: 0; background: #16a34a; transition: width .1s; }
  .fill.warn { background: #ea580c; }
  .row { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; color: #374151; }
  #last { font-family: ui-monospace, Consolas, monospace; }
</style>
</head>
<body>
<div class="card">
  <label for="t">Short note (80 characters max)</label>
  <textarea id="t" placeholder="Type or paste here"></textarea>
  <div class="bar"><div class="fill" id="fill"></div></div>
  <div class="row"><span id="count">0 / 80</span><span id="last">last change: none</span></div>
</div>

<script>
  const MAX = 80;
  const t = document.getElementById('t');
  const fill = document.getElementById('fill');

  // input fires after every change: typing, paste, delete
  t.addEventListener('input', (e) => {
    if (t.value.length > MAX) t.value = t.value.slice(0, MAX);   // trim pasted overflow
    document.getElementById('count').textContent = t.value.length + ' / ' + MAX;
    fill.style.width = (t.value.length / MAX * 100) + '%';
    fill.classList.toggle('warn', t.value.length >= MAX - 10);
    document.getElementById('last').textContent = 'last change: ' + e.inputType;
  });
</script>
</body>
</html>
Type, paste a long text, or press Backspace. The counter stops at 80.

Three lines do the work:

  • Trim: if value.length is over the limit, cut it with slice(0, MAX).
  • Show: write value.length into the counter element.
  • Report: read e.inputType to say what just happened.

To read the value in other ways, see get input value in JavaScript.

When it does not work

What you see Cause Fix
The textInput listener never runs The name is spelled textinput Use textInput, or better, input
Old textInput code works in some browsers only MDN's data lists Firefox support from version 129 Switch to beforeinput and input
Letters still appear after preventDefault The change was a paste, autofill or IME Also clean the value in input
e.data is null The change inserted no text, such as a delete Test e.data before using it
The counter is one character behind The value was read in keydown or beforeinput, before the change Read it in input
The listener on a div never runs The div is not editable Add contenteditable, or listen on an input element
change does not fire while typing It waits until the value is committed Use input for live updates

Text events are easiest to understand by typing into them, and a screenshot of the log cannot be typed into. Typing, pasting and autofill also behave a little differently on every device, so it helps to let the other person try on theirs.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can type into the fields themselves. If you change the code later, the same link shows the new version.

Questions people ask

Is TextEvent deprecated?

Yes. MDN marks the TextEvent interface as deprecated and says to avoid it in new projects. The UI Events specification lists textInput among its legacy event types. MDN names input, beforeinput, keydown and keyup as the events that superseded it.

Is the event called textinput or textInput?

The UI Events specification spells it textInput, with a capital I. In a Chromium test, a listener added for lowercase textinput never fired, while textInput did. Spell it exactly if you ever have to read it.

Which event should I use to react to typing in a text box?

Use input. It fires after the value has changed, whether the change came from typing or pasting. Use beforeinput only when you need to see the change before it lands.

What is the difference between input and change?

input fires every time the value changes as a direct result of a user action. change fires only when the value is committed, such as when the field loses focus.

Can I stop a character from being typed?

Yes, with a caveat: call preventDefault() inside a beforeinput listener. Browsers do not always let you cancel it, though. MDN lists autocomplete, spell-check corrections, password-manager autofill and IME as cases where it may not be cancelable, so clean the value in an input listener too.

Keep reading