React UIEvent: what the browser gives you, and what React hands your handler

UIEvent is the web-platform interface behind clicks, key presses, focus, input and wheel events. It adds two fields, detail and view, and React wraps it in an object of the same shape.

UIEvent is the browser interface for simple user-interface events. It is the parent of MouseEvent, KeyboardEvent, FocusEvent, InputEvent and WheelEvent, and it adds two fields: detail and view.

This page uses the web-platform meaning, because that is what a plain HTML file runs. People who type "react uievent" usually mean React.UIEvent, the type of the object a React handler receives. That is covered below too.

Try it first. Click, double-click, type, and scroll the small box. Each row appears when that event fires.

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>Which events are UIEvents?</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 12px; font: 14px/1.4 system-ui, sans-serif; color: #1d2330; background: #fff; }
  .tools { display: flex; flex-wrap: wrap; gap: 8px; align-items: stretch; margin-bottom: 10px; }
  button, input { font: inherit; padding: 8px 10px; border: 1px solid #c9ced8; border-radius: 8px; background: #f4f5f7; }
  input { background: #fff; min-width: 0; flex: 1 1 120px; }
  .box { width: 120px; height: 40px; overflow: auto; border: 1px solid #c9ced8; border-radius: 8px; font-size: 12px; }
  .box div { height: 120px; padding: 4px 6px; }
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  th, td { text-align: left; padding: 4px 6px; border-bottom: 1px solid #e5e7eb; }
  th { color: #6b7280; font-weight: 600; }
  td:first-child { font-family: ui-monospace, Consolas, monospace; }
  .yes { color: #0f5132; font-weight: 700; }
  .no { color: #9a3412; font-weight: 700; }
  p { margin: 8px 0 0; font-size: 12.5px; color: #4b5563; }
</style>
</head>
<body>
<div class="tools">
  <button id="btn">Click me</button>
  <input id="txt" placeholder="Type here">
  <div class="box" id="box" tabindex="0"><div>Scroll or wheel here</div></div>
</div>
<table>
  <thead><tr><th>Event</th><th>Object</th><th>UIEvent?</th><th>detail</th><th>view</th></tr></thead>
  <tbody id="rows"></tbody>
</table>
<p>Each row appears when that event fires. Resize the window to see <b>resize</b>.</p>

<script>
  const rows = document.getElementById('rows');
  const types = ['click', 'dblclick', 'mousedown', 'keydown', 'focusin', 'input', 'wheel', 'scroll'];
  const seen = {};

  function show(e) {
    let tr = seen[e.type];
    if (!tr) {
      tr = seen[e.type] = rows.insertRow();
      for (let i = 0; i < 5; i++) tr.insertCell();
    }
    const isUI = e instanceof UIEvent;
    tr.cells[0].textContent = e.type;
    tr.cells[1].textContent = e.constructor.name;
    tr.cells[2].textContent = isUI ? 'yes' : 'no';
    tr.cells[2].className = isUI ? 'yes' : 'no';
    tr.cells[3].textContent = isUI ? e.detail : '-';
    tr.cells[4].textContent = isUI ? (e.view === window ? 'window' : String(e.view)) : '-';
  }

  // capture on document so non-bubbling events (scroll, focus) are seen too
  types.forEach((t) => document.addEventListener(t, show, true));
  window.addEventListener('resize', show);
</script>
</body>
</html>
One listener per event type, on document. Each row shows the event object's class, whether it is a UIEvent, and its detail and view.

Click rows show PointerEvent and keyboard rows show KeyboardEvent. They all say "yes" because those interfaces inherit from UIEvent. The scroll row says "no" in some browsers, which is the catch explained further down.

Which events are UIEvents

MDN lists these interfaces as inheriting from UIEvent. Every event object of those types therefore has detail and view.

UIEvent sits between Event and the mouse, keyboard, focus, input, wheel, touch and composition events.
UIEvent sits between Event and the mouse, keyboard, focus, input, wheel, touch and composition events.

PointerEvent inherits from MouseEvent, so a pointer event has the fields too. In our test a click arrived as a PointerEvent in Chromium, Firefox and WebKit.

Event Object we saw detail
click PointerEvent 1 for a single click
dblclick MouseEvent 2
keydown KeyboardEvent 0
focusin FocusEvent 0
input InputEvent 0
wheel WheelEvent 0

MDN's page for the old UIEvent.which property marks it as deprecated. For new code, use KeyboardEvent.key or code for keys, and MouseEvent.button for mouse buttons.

detail: the click count

For click and dblclick, detail is the current click count. For mousedown and mouseup, MDN says it is 1 plus the current click count. On every other UIEvent it is zero.

That makes a triple-click check one line:

el.addEventListener('click', (e) => {
  if (e.detail === 3) selectWholeParagraph();  // third click in a row
});

Try a triple click below. The second box builds a UIEvent by hand.

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>UIEvent detail and the constructor</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 12px; font: 14px/1.45 system-ui, sans-serif; color: #1d2330; background: #fff; }
  h2 { font-size: 14px; margin: 0 0 6px; }
  .pad {
    padding: 22px 12px; text-align: center; border-radius: 12px; cursor: pointer;
    background: #eef2ff; border: 1px solid #c7d2fe; user-select: none; -webkit-user-select: none;
  }
  .pad b { font-size: 22px; display: block; }
  .out { font: 13px/1.5 ui-monospace, Consolas, monospace; background: #f4f5f7; border-radius: 8px; padding: 8px 10px; margin: 8px 0 18px; min-height: 3.2em; white-space: pre-wrap; }
  button { font: inherit; padding: 8px 12px; border: 1px solid #c9ced8; border-radius: 8px; background: #f4f5f7; }
</style>
</head>
<body>
<h2>1. Click count from e.detail</h2>
<div class="pad" id="pad"><b id="n">0</b>Click, double-click, triple-click</div>
<div class="out" id="out1">Waiting for a click.</div>

<h2>2. Build your own UIEvent</h2>
<button id="make">Dispatch new UIEvent('hello')</button>
<div class="out" id="out2">Nothing dispatched yet.</div>

<script>
  const pad = document.getElementById('pad');
  const n = document.getElementById('n');
  const out1 = document.getElementById('out1');
  let down = '-';

  // mousedown reports 1 + the click count so far; click reports the count itself
  pad.addEventListener('mousedown', (e) => { down = e.detail; });
  pad.addEventListener('click', (e) => {
    n.textContent = e.detail;
    out1.textContent = 'mousedown detail: ' + down + '\nclick detail:     ' + e.detail;
  });

  // new UIEvent(type, { detail, view }) - the page makes it, so it is not trusted
  const target = document.getElementById('out2');
  target.addEventListener('hello', (e) => {
    target.textContent = 'type: ' + e.type + '\ndetail: ' + e.detail +
      '\nview === window: ' + (e.view === window) + '\nisTrusted: ' + e.isTrusted;
  });
  document.getElementById('make').addEventListener('click', () => {
    target.dispatchEvent(new UIEvent('hello', { detail: 7, view: window }));
  });
</script>
</body>
</html>
Triple-click the blue pad to see detail count up. The button dispatches a UIEvent the page created itself.

view, and building your own UIEvent

view is the window the event happened in. In a normal page it is the same as window, which the demo checks with e.view === window.

In our Chromium test, view was null on input events, while Firefox and WebKit gave the window. Treat view as optional on input.

You can make your own with the constructor. Both options are optional, detail defaults to 0 and view to null:

const ev = new UIEvent('hello', { detail: 7, view: window });
box.dispatchEvent(ev);

Events built this way are not trusted. ev.isTrusted is false, and MDN notes that only trusted events trigger default actions. Use a hand-made event to run your own listeners.

React UIEvent: what your handler receives

React does not pass the browser event directly. Its docs call the object a React event object, sometimes a "synthetic event". It copies the standard fields and keeps the original in nativeEvent.

A plain listener gets the browser event. A React prop gets a wrapper that keeps the browser event in nativeEvent.
A plain listener gets the browser event. A React prop gets a wrapper that keeps the browser event in nativeEvent.

React's docs describe a "UIEvent handler" for generic UI events, with onScroll as the example, and say its event object has the extra properties detail and view. In the typings, the mouse, keyboard and wheel event types extend it.

The demo below loads React 18.3.1 from unpkg and writes components with React.createElement, so it runs as one HTML file with no build step.

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>React UIEvent handlers</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 12px; font: 14px/1.45 system-ui, sans-serif; color: #1d2330; background: #fff; }
  .row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
  .pad, .scroller {
    flex: 1 1 150px; border-radius: 10px; border: 1px solid #c9ced8; background: #eef2ff;
    padding: 14px 10px; text-align: center; user-select: none; -webkit-user-select: none; cursor: pointer;
  }
  .scroller { background: #fff; height: 78px; overflow: auto; text-align: left; cursor: auto; padding: 0; }
  .scroller div { height: 240px; padding: 8px 10px; }
  pre { margin: 0; padding: 8px 10px; background: #f4f5f7; border-radius: 8px; font: 12.5px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; min-height: 9.2em; }
  h2 { font-size: 13px; margin: 0 0 6px; color: #4b5563; }
</style>
</head>
<body>
<div id="root"></div>

<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script>
  const h = React.createElement;

  function App() {
    const [info, setInfo] = React.useState('Click the pad, press a key in the box, or scroll the list.');

    // one function for every handler: it only reads UIEvent fields
    function report(e) {
      setInfo(
        'handler event: ' + e.type +
        '\ne.detail: ' + e.detail +
        '\ne.view: ' + (e.view === window ? 'window' : String(e.view)) +
        '\nnativeEvent: ' + e.nativeEvent.constructor.name +
        '\nnative is a UIEvent: ' + (e.nativeEvent instanceof UIEvent)
      );
    }

    return h('div', null,
      h('div', { className: 'row' },
        h('div', { className: 'pad', onClick: report }, 'onClick'),
        h('input', { placeholder: 'onKeyDown: type here', onKeyDown: report, style: { flex: '1 1 150px', font: 'inherit', padding: '8px' } })
      ),
      h('div', { className: 'row' },
        h('div', { className: 'scroller', onScroll: report }, h('div', null, 'onScroll: scroll this list'))
      ),
      h('h2', null, 'What the handler received'),
      h('pre', null, info)
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(h(App));
</script>
</body>
</html>
Click the pad, press a key, scroll the list. One function reads detail, view and nativeEvent from each React event.

React 19 no longer ships UMD builds, according to the React 19 upgrade guide, which suggests an ESM CDN such as esm.sh for script-tag use. This page's demo pins 18.3.1, which has the UMD file.

In a project with a build step, you type the handler yourself. This is TypeScript and needs a compiler, so it does not run in a plain HTML file:

import type { UIEvent } from 'react';

function onScroll(e: UIEvent<HTMLDivElement>) {
  console.log(e.detail, e.view);   // detail: number, view: AbstractView
  console.log(e.nativeEvent);      // the browser's own event
}

// <div onScroll={onScroll}>...</div>

In the DefinitelyTyped React typings, the type is declared like this, and onScroll takes a handler of it:

interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
  detail: number;
  view: AbstractView;
}

Importing it shadows the global DOM UIEvent in that file, so write React.UIEvent if you need both.

The scroll and resize catch

The types say detail is a number. At runtime that only holds if the browser really sent a UIEvent. MDN calls both scroll and resize generic Event objects.

In our test the scroll event was a plain Event in two browsers, and React passed detail along as undefined.
In our test the scroll event was a plain Event in two browsers, and React passed detail along as undefined.

In the demo above, scroll the list in a Chromium or WebKit browser and e.detail reads undefined. Firefox sent a UIEvent and showed 0.

So read detail and view from click, key, focus, input and wheel handlers, and leave scroll and resize alone. resize fires only on window; for one element, use ResizeObserver.

When it does not work

What you see Cause Fix
e.detail is undefined in onScroll The native scroll event is a plain Event in that browser Do not read detail on scroll
e.view is null in an input handler Some browsers leave view empty on input events Use window directly
Code reads e.which MDN marks it deprecated Use key, code or button
A resize listener on a div never fires resize fires only on window Use ResizeObserver
A hand-made event triggers no default action Events from new UIEvent are not trusted Call the method directly
TypeScript: handler on a React prop is mistyped The DOM UIEvent type was used for a React prop Type it as React.UIEvent
React is not defined in a plain HTML file The React script did not load or came after yours Load React first, then your code

A UIEvent is easier to understand by firing one than by reading a table, and a screenshot of the log cannot be clicked. For related events, see pointer events in JavaScript and addEventListener.

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, including the React one loaded from unpkg, so the people you send it to can click and scroll it themselves. If you change the code later, the same link shows the new version.

Questions people ask

Is React.UIEvent the same thing as the browser UIEvent?

No. The browser UIEvent is the DOM interface. React.UIEvent is the type of the React event object, which wraps the browser event and keeps the original in nativeEvent. Both carry detail and view.

Why is e.detail undefined in my onScroll handler?

In our tests the native scroll event was a plain Event in Chromium and WebKit, which has no detail, so React passed undefined along. Firefox sent a UIEvent and detail was 0. Do not rely on detail or view in scroll handlers.

Can I create a UIEvent myself?

Yes, with new UIEvent(type, options), where options can set detail and view. Events made by your code are not trusted: isTrusted is false, and MDN says only trusted events trigger default actions. Use it to trigger your own listeners.

Does the resize event give me a UIEvent?

MDN lists resize as a generic Event, and it was a plain Event in all three browsers we tested. It fires only on window. To watch the size of one element, use a ResizeObserver.

Should I still read e.which?

No. MDN marks UIEvent.which as deprecated. Read KeyboardEvent.key or KeyboardEvent.code for keys, and MouseEvent.button for mouse buttons.

Keep reading