what-input
v6.0.1
Published
A global utility for tracking the current input method (mouse, keyboard or touch).
Maintainers
Readme
What Input?
A global utility for tracking the current input method (mouse, keyboard or touch).
What Input is now v6
Rebuilt for the modern web: Pointer Events, KeyboardEvent.key, TypeScript, native ES modules, and an EventTarget API. The outputs are the same as v5, so it's a drop-in upgrade for existing sites. See the changelog for the few things that changed.
What Input adds data attributes to the <html> element based on the type of input being used. It also exposes a simple API that can be used for scripting interactions.
How it works
What Input listens on the window (in the capture phase, so local stopPropagation() calls don't hide anything) for:
pointerdown– a direct interaction.pointerTypedecides whether it'smouseortouch(penis reported astouch).keydown/keyup– a directkeyboardinteraction, unless the key is on the ignore list (modifiers by default).pointermove/wheel– an indirect signal that a mouse is present.focusin/focusout– which element currently has focus.
It then sets or updates these attributes on <html>:
| Attribute | Values | Updated by |
| ------------------ | ------------------------------------------- | ------------------------------------------------------ |
| data-whatinput | initial, mouse, keyboard, touch | pointerdown, keydown, keyup |
| data-whatintent | initial, mouse, keyboard, touch | everything above plus pointermove and wheel |
| data-whatelement | the focused element's tag name, e.g. a | focusin / focusout |
| data-whatclasses | the focused element's classes, e.g. a,b,c | focusin / focusout (only when the element has any) |
What Input does not make assumptions about the input environment before the page is interacted with. However, the pointermove and wheel events are used to set data-whatintent="mouse" to indicate that a mouse is being used indirectly.
Interacting with forms
Since interacting with a form always requires use of the keyboard, What Input uses the data-whatintent attribute to display a "buffered" version of input events while <input>s, <select>s, <textarea>s, contenteditable elements, form-associated custom elements and <button>s inside a <form> are being interacted with (i.e. a mouse user's data-whatintent will be preserved as mouse while typing). data-whatinput always reflects the actual last input.
Checkboxes and radio buttons are toggled rather than typed into, so pressing a key on one does switch the intent to keyboard.
The same buffering can be opted into for any other element by adding a data-whatfreeze attribute to it or to a container. This is handy for custom controls that are operated with the arrow keys, like a slider or a video scrubber, where pressing a key is not a change of intent:
<div class="scrubber" data-whatfreeze>
<div class="scrubber-handle" role="slider" tabindex="0" ...></div>
</div>Device hint
Before anyone interacts with the page, data-whatinput and data-whatintent are initial on purpose. When a best guess is still useful (sizing hit targets on first paint, say), the data-whathint attribute carries what the device's primary pointer suggests, read from the pointer interaction media feature:
| data-whathint | Media query | Typically |
| --------------- | ------------------- | ---------------------------- |
| touch | (pointer: coarse) | phones, tablets |
| mouse | (pointer: fine) | desktops, laptops, trackpads |
| unknown | neither | keyboard-only devices, TVs |
The hint follows the device live (a mouse paired with a tablet, a keyboard detached from one) and is never persisted. It is only a hint: an iPad with a trackpad still reports touch, and a touch-screen laptop reports mouse. data-whatinput and data-whatintent never use it.
Isn't this what :focus-visible is for?
Partly. :focus-visible is a great default for focus rings and you should use it. What Input is still useful when you want to:
- style anything other than focus (hover affordances, hit targets, tooltips) based on how the page is being used;
- know the input method from JavaScript;
- tell
mouseandtouchapart; - remember the input method across page loads so the first render is already right.
Demo
Check out the demo to see What Input in action.
https://ten1seven.github.io/what-input
Installing
Install via npm (or your package manager of choice):
npm install what-inputOr load it from a CDN:
<script src="https://unpkg.com/what-input@6"></script>Or download dist/what-input.js and include it directly.
Usage
Include the script directly in your project. It exposes a global whatInput.
<script src="path/to/what-input.js"></script>Or import it. The package ships as an ES module (dist/what-input.mjs), as a UMD build (dist/what-input.js, which also covers CommonJS and AMD) and with TypeScript declarations.
import 'what-input'
// or
import whatInput from 'what-input'
// or
const whatInput = require('what-input')What Input will start doing its thing while you do yours.
Bundlers: importing the module has the side effect of starting What Input. The package declares
"sideEffects": trueso a bareimport 'what-input'is never tree-shaken away.
Default behavior
Persisting input/intent across pages
By default, What Input uses session storage to persist the input and intent values across pages. The benefit is that once a visitor has interacted with the page, subsequent pages won't have to wait for interactions to know the input method.
This behavior can be disabled by adding a data-whatpersist="false" attribute on either the <html> or <body>.
<html dir="ltr" lang="en" data-whatpersist="false">
...
</html>or
<body data-whatpersist="false">
...
</body>Session storage can be cleared at any time with:
whatInput.clearStorage()Basic styling
/*
* only suppress the focus ring once what-input has successfully started
*/
/* suppress focus ring on form controls for mouse users */
[data-whatintent='mouse'] *:focus {
outline: none;
}A more modern take pairs What Input with :focus-visible and only reaches for the data attributes where the browser heuristic isn't enough:
:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* bigger hit areas while a touch screen is being used */
[data-whatintent='touch'] .toolbar button {
min-block-size: 44px;
}Note: If you remove outlines with outline: none;, be sure to provide clear visual :focus styles so the user can see which element they are on at any time for greater accessibility. Visit W3C's WCAG 2.2 2.4.7 Guideline to learn more.
Scripting
Current input
Ask What Input what the current input method is. This works best if asked after the events What Input is bound to (pointerdown and keydown).
whatInput.ask() // returns `mouse`, `keyboard` or `touch`
myButton.addEventListener('click', () => {
if (whatInput.ask() === 'mouse') {
// do mousy things
} else if (whatInput.ask() === 'keyboard') {
// do keyboard things
}
})If it's necessary to know if pointermove is being used, use the 'intent' option. For example:
/*
* nothing has happened but the mouse has moved
*/
whatInput.ask() // returns `initial` because the page has not been directly interacted with
whatInput.ask('intent') // returns `mouse` because mouse movement was detected
/*
* the keyboard has been used, then the mouse was moved
*/
whatInput.ask() // returns `keyboard` because the keyboard was the last direct page interaction
whatInput.ask('intent') // returns `mouse` because mouse movement was the most recent action detectedSince click events are PointerEvents in current browsers, a single click handler can also read event.pointerType directly: it's mouse, pen or touch, and empty when the click came from the keyboard.
Device hint
whatInput.ask('hint') // returns `mouse`, `touch` or `unknown`Current element
Ask What Input the currently focused DOM element.
whatInput.element() // returns a string, like `input`, or nullIgnore keys
Set a custom list of keys that will be ignored (will not switch over to keyboard) when pressed. Keys can be KeyboardEvent.key values, KeyboardEvent.code values or legacy keycodes. A custom list will overwrite the default values.
/*
* default ignored keys:
* 'Shift'
* 'Control'
* 'Alt'
* 'Meta' // Windows key / Apple cmd
* 'ContextMenu' // Windows menu key
*/
whatInput.ignoreKeys(['Shift', 'CapsLock', 'F11'])
// legacy keycodes still work
whatInput.ignoreKeys([16, 20, 122])Specific keys
Set a custom list of keys that will trigger the keyboard pressed intent (will not switch to keyboard unless these keys are pressed). This overrides ignoreKeys.
// only listen to tab keyboard press
whatInput.specificKeys(['Tab'])
// go back to using the ignore list
whatInput.specificKeys([])Custom callbacks
Fire a function when the input or intent changes.
// create a function to be fired
const myFunction = (type) => {
console.log(type)
}
// fire `myFunction` when the intent changes
whatInput.registerOnChange(myFunction, 'intent')
// fire `myFunction` when the input changes
whatInput.registerOnChange(myFunction, 'input')
// fire `myFunction` when the device hint changes
whatInput.registerOnChange(myFunction, 'hint')
// remove custom event
whatInput.unRegisterOnChange(myFunction)Events
whatInput is an EventTarget. It dispatches input, intent and hint CustomEvents whose detail is the new value, so all the usual listener options (once, signal, ...) are available.
whatInput.addEventListener('intent', (event) => {
console.log(event.detail) // `mouse`, `keyboard` or `touch`
})
// listen until a component unmounts
const controller = new AbortController()
whatInput.addEventListener('input', update, { signal: controller.signal })
controller.abort()In browsers that ship the Observable API, whatInput.when('intent') works too, for free.
Tearing down
Stop listening, remove the data-what* attributes and drop every callback. Mostly useful in tests and when an app is unmounted.
whatInput.destroy()TypeScript
Types ship with the package.
import whatInput, { type InputMethod, type Strategy } from 'what-input'
const current: InputMethod = whatInput.ask('intent')Server-side rendering
What Input can be imported in Node, workers and other environments without a DOM. It won't observe anything, ask() always answers initial and element() always answers null.
Shadow DOM
What Input reports focus inside open shadow roots and knows when a keypress lands in a field inside one. The attributes still live on <html>, and selectors in the main document don't reach into a shadow tree, so style from inside the component instead. :host-context() works in Chromium and Safari; for every browser, mirror the state onto the host with the events:
class MyField extends HTMLElement {
connectedCallback() {
this.controller = new AbortController()
whatInput.addEventListener(
'intent',
(event) => this.setAttribute('data-whatintent', event.detail),
{ signal: this.controller.signal },
)
}
disconnectedCallback() {
this.controller.abort()
}
}/* inside the shadow root */
:host([data-whatintent='mouse']) :focus {
outline: none;
}Testing
The state only changes in response to real events, which is also the easiest way to drive it in tests. In a DOM test environment that implements PointerEvent (jsdom 22+, happy-dom, every browser):
window.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'touch' }))
whatInput.ask() // `touch`
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
whatInput.ask() // `keyboard`Or mock the module entirely:
vi.mock('what-input', () => ({ default: { ask: () => 'touch' } }))Compatibility
What Input v6 works in all evergreen browsers: Chrome and Edge 90+, Firefox 90+ and Safari 15+ (2021 and newer). It relies on Pointer Events, AbortSignal support in addEventListener(), EventTarget subclassing and class private fields, none of which are polyfilled.
The UMD builds (dist/what-input.js and dist/what-input.min.js) are emitted as ES2019 syntax so that older bundlers such as webpack 4 can still parse them. The ES module build (dist/what-input.mjs) targets ES2022. The browser requirements are the same for both.
If you need to support older browsers, stay on v5.
Contributing
npm install # install dependencies
npm run dev # run the demo with hot reloading
npm test # build the library and run the tests
npm run lint # prettier and type checks
npm run build # build the library and the demo siteChangelog
v6.0.1
- Fixed: The UMD builds are emitted as ES2019 syntax (no class fields, private fields, optional chaining or nullish coalescing) so that older bundlers such as webpack 4 can parse them. Foundation's open-ended
what-inputpeer range means v6 reaches a lot of older build pipelines on fresh installs. The ES module build still targets ES2022 and browser support is unchanged.
v6.0.0
A rewrite in TypeScript on modern browser APIs. The data attributes, the session storage keys, the API and the dist/what-input.js file path are unchanged, so for most sites this is a drop-in upgrade.
- Breaking: Drops support for Internet Explorer and other legacy browsers. The build targets ES2022 and is no longer transpiled.
- Breaking: Detection is built on Pointer Events only. The
mousedown,mousemove,touchstart,touchend,MSPointerDown,MSPointerMove,mousewheelandDOMMouseScrolllisteners (and the 200ms touch-versus-mouse buffer they required) are gone. - Breaking:
clearStorage()now removes only What Input's ownwhat-inputandwhat-intentkeys instead of wiping the entire session storage. - Breaking:
pointerdownnow updates the intent as well as the input, even while a form field has focus. Only keyboard use inside form fields is buffered. - Breaking:
unRegisterOnChange(fn)removesfnfrom both theinputandintentlists. - Breaking: The
InputMethodtype no longer includespointer, which was never actually reported. - Breaking: Requires Node 20+ for development. The build uses esbuild, the demo uses Vite and the tests use Vitest; Gulp, Webpack, Babel, Sass and Yarn are gone.
- Added: A
data-whathintattribute,ask('hint')and ahintevent report what the device's primary pointer suggests (mouse,touchorunknown) from thepointerinteraction media feature, before any interaction and live as devices are attached or removed. The existing attributes never use it. - Added: A
data-whatfreezeattribute opts any element (or container) into the same intent buffering that form fields get, for custom controls driven by the arrow keys (#123). - Added: An ES module build (
dist/what-input.mjs) with a properexportsmap,moduleandunpkgfields. The UMD build (dist/what-input.js) and the minified build remain for<script>tags, CommonJS and AMD. - Added:
whatInputis anEventTargetand dispatchesinputandintentevents. - Added:
whatInput.destroy()tears everything down (listeners are bound with anAbortController). - Added:
ignoreKeys()andspecificKeys()acceptKeyboardEvent.keyandKeyboardEvent.codestrings alongside legacy keycodes. The defaults are now key names. - Added: Typing inside
contenteditableelements and form-associated custom elements no longer changes the intent, just like form fields. - Fixed: Pressing a key on a checkbox or radio button switches the intent to
keyboard; they are toggled, not typed into (#128). - Fixed: Long-presses in Firefox for Android no longer flash
data-whatintent="mouse", because the emulated mouse events are no longer listened to (#94). - Added: Focus inside open shadow roots is reported for the inner element (via
composedPath()), and the active-element check follows shadow roots when deciding whether a keypress is text entry. - Added: Persisted session storage values are validated before they are used.
- Changed: Type declarations are generated from the source and shipped in
dist/. - Changed:
pointerdownandkeydownare observed in the capture phase onwindow, sowhatInput.ask()is already up to date inside any element's own event handler, even one that callspreventDefault()orstopPropagation()(#80). - Changed: When the script runs after
DOMContentLoaded(dynamicimport(), late-loaded scripts) the initial state is published immediately instead of never. - Changed: An interaction that happens before
DOMContentLoadedis no longer overwritten by the persisted value. - Fixed:
data-whatclassesseparates all classes with commas (a,b,c); previously only the first space was replaced. - Fixed:
data-whatclassesis removed when focus moves to an element without classes.
v5.2.12
- Fixed: Improved detection and respect for of
data-whatpersistbeforeDOMContentLoaded. Fix via FanFataL.
v5.2.11
- Fixed: Adds
useCaptureso events can be detected beforepreventDefaultcancels them on local listeners (h/t jojo080889).
v5.2.8 - 5.2.10
- Added: TypeScript definitions via greypants.
v5.2.7
- Fixed: Moves sourcemap to sit next to JavaScript package.
v5.2.6
- Fixed: adds
DOMContentLoadedevent before looking fordata-whatpersistattribute on body.
v5.2.5
- Fixed: Hotfix for shaky support of
datasetin IE10.
v5.2.4
- Added: Ability to add
data-whatpersist="false"attribute to the<html>or<body>tag to disable usage of session storage to persist input/intent across pages. - Updated: Build tools and added linting.
v5.2.3
- Fixed:
activeElementis null bug in IE is fixed (thanks @EasterPeanut). - Fixed: Mousewheel event detection works correctly again.
v5.2.1
- Fixed: iOS was occasionally reporting
mousebecause of event execution order. - Added:
touchendto input map - Added: Allows buttons inside forms to be treated like other form inputs.
- Added: iTouch intent indicator in demo page (it worked all along, you just couldn't see it).
v5.1.4
- Fixed: Increase buffering time by 20ms to fix iOS reporting mousedown
- Fixed: Adds
touchendto input map
v5.1.3
- Added: Sourcemap for the minified version.
v5.1.2
- Added:
specificKeysfunctionality to allow overriding of keyboard keys list. Fix via bk3.
v5.1.1
- Fixed: Browsers with cookies turned off would throw an error with session storage. Fix via yuheiy.
v5.1.0
- Added: Session variable stores last used input and intent so subsequent page loads don't have to wait for interactions to set the correct input and intent state.
- Removed: IE8 support.
v5.0.7
- Fixed:
unRegisterOnChangefailed to unregister items at index 0.
v5.0.5
- Fixed: Fail gracefully in non-DOM environments.
v5.0.3
- Fixed: Event buffer for touch was not working correctly.
Changes from v4
- Added: A the ability to add and remove custom callback function when the input or intent changes with
whatInput.registerOnChangeandwhatInput.unRegisterOnChange. - Added: A
data-whatelementattribute exposes any currently focused DOM element (i.e.data-whatelement="a"ordata-whatelement="input"). - Added: A
data-whatclassesattribute exposes any currently focused element's classes as a comma-separated list (i.e.data-whatclasses="class1,class2"). - Added: An API option to provide a custom array of keycodes that will be ignored.
- Changed: Typing in form fields is no longer filtered out. The
data-whatinputattribute immediately reflects the current input. Thedata-whatintentattribute now takes on the role of remembering mouse input prior to typing in or clicking on a form field. - Changed: If you use the Tab key to move from one input to another one - the
data-whatinputattribute reflects the current input (switches to "keyboard"). - Removed:
whatInput.types()API option. - Removed: Bower support.
- Fixed: Using mouse modifier keys (
shift,control,alt,cmd) no longer toggles back to keyboard.
Acknowledgments
Special thanks to Viget for their encouragement and commitment to open source projects. Visit code.viget.com to see more projects from Viget.
Thanks to mAAdhaTTah for the initial conversion to Webpack. Thanks to greypants for adding TypeScript definitions.
What Input is written and maintained by @ten1seven.
License
What Input is freely available under the MIT License.
