npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ftyyy/mouseless

v0.0.9

Published

`mouseless` is a React library for defining high-level keyboard interactions.

Downloads

11

Readme

Mouseless

mouseless is a React library for defining high-level keyboard interactions.

Installation

Install from npm:

npm install @ftyyy/mouseless

Feature Highlights

See this demo page for the interactive examples.

Code below are simplified. For full example code, see the example folder.

Track Held Keys

Detect when specific key combinations (e.g. ctrl + s) are being held. (Also notice how default browser behavior of ctrl + s is suppressed.)

import { useKeyHoldingState } from "@ftyyy/mouseless"
function MyComponent(){
    const holding = useKeyHoldingState(["Ctrl", "s"])

    return holding && <CatDance />
}

illu_holding.gif

Navigate UI with the Keyboard

Move between UI elements and simulates clicking with keyboard.

import { 
    useKeyHoldingState,
    useSpaceNavigatorState, 
    useKeyEventsHandlerRegister,
} from "@ftyyy/mouseless"

function MyComponent({onClick}){
    const keys = ["Alt", "w"]
    const holding = useKeyHoldingState(keys)
    const [space, node] = useSpaceNavigatorState()
    const [ 
        add_handler, del_handler
    ] = useKeyEventsHandlerRegister()

    React.useEffect(()=>{
        const handler = ()=>(
            (space == "my_space") && onClick(node)
        )
        add_handler(keys, "Enter", false, handler)
        return ()=>{
            del_handler(keys,"Enter", false, handler)
        }
    }, [space , node])

    return (holding && (space == "my_space")) && (
        <Penel cur_node={node} />
    )
}

illu_navi.gif

Move Elements with Keyboard

Simulate "drag and move" behavior with arrow keys.

import { useKeyHoldingState } from "@ftyyy/mouseless"
function MyComponent(){
    const keys = ["Alt", "r"]
    const holding = useKeyHoldingState(keys)
    const L = useKeyHoldingState([...keys, "ArrowLeft"])
    const R = useKeyHoldingState([...keys, "ArrowRight"])
    const U = useKeyHoldingState([...keys, "ArrowUp"])
    const D = useKeyHoldingState([...keys, "ArrowDown"])
    const [pos, set_pos] = React.useState({x: 0, y: 0})

    React.useEffect(()=>{
        const interval = setInterval(() => {
            let dx = (R ? 4 : 0) - (L ? 4 : 0)
            let dy = (D ? 4 : 0) - (U ? 4 : 0)
            set_pos({x: pos.x + dx, y: pos.y + dy})
        }, 10)
        return ()=>{clearInterval(interval)}
    }, [pos, L, R, U, D])

    return holding && <Plane x={pos.x} y={pos.y} />
}

illu_moving.gif

Detailed Usage

Wrap all keyboard-aware components inside a KeyEventManager, which delegates keyDown and keyUp events. Use preventing_default to suppress default browser behavior for specified key combinations.

import * as React from "react"
import { KeyEventManager, KeyNames } from "@ftyyy/mouseless"

function App(){
    return <KeyEventManager
        preventing_default = {[KeyNames.ctrl, KeyNames.s]} // prevent ctrl+s
    >{(keydown_proxy, keyup_proxy) => (
        <div 
            tabIndex  = {0}
            onKeyDown = {keydown_proxy}
            onKeyUp   = {keyup_proxy}
        >
            <YourComponent />
        </div>
    )}</KeyEventManager>
}

Concepts

mouseless will provide information of the following states/events:

  • holding: for a key combination that is being pressed & held, they are called holding keys. mouseless keeps track of the state of all holding keys.
  • pressing / releasing events: when a key that is being pressed or being released, it will trigger an event (just like the original keyDown and keyUp events).
  • the combination of these two types of information.

Key Names

mouseless provides an object KeyNames to define the names of keys. But you can also just use strings as key names.

import { KeyNames } from "@ftyyy/mouseless"

console.log(KeyNames.ctrl)    // "Control"
console.log(KeyNames.Control) // "Control"

Hooks

mouseless provides several hooks for accessing keyboard states and listening to high-level keyboard events.

  • useKeyHoldingState

    Returns true if the given key combination is currently being held.

    import * as React from "react"
    import { 
        useKeyHoldingState , 
        KeyNames , 
    } from "@ftyyy/mouseless"
    
    function YourComponent(){
        // whether a combination of keys are pressed
        const holding = useKeyHoldingState([KeyNames.ctrl, KeyNames.s])
    
        return holding ? <p>pressed ctrl + s</p> : <></>
    }
  • useKeyEventsHandlerRegister

    Returns [add_handler, del_handler] functions to register/unregister high-level key event listeners.

    add_handler listens to the event that a key is pressing or releasing, while a combination of keys are being held. Unlike the original keyDown event, even when the pressing key is kept held after pressing, the event will only be triggered once.

    add_handler: (
        KeyName[],              // key combination that needs to be held to trigger the event
        KeyName,                // key that needs to be pressing / releasing to trigger the event
        boolean,                // `true`: listen to key releasing; `false`: listen to key pressing
        (e: KeyEvent)=>void ,   // callback function when the event is triggered.
    ) => void

    The del_handler has the same arguments with add_handler.

    import * as React from "react"
    import { 
        useKeyEventsHandlerRegister , 
        KeyNames , 
    } from "@ftyyy/mouseless"
    
    function YourComponent(){
        const [add_handler, del_handler] = useKeyEventsHandlerRegister()
    
        React.useEffect(()=>{
            const handler_1 = ()=>{
                console.log("ctrl+w+enter pressed!")
            }
            const handler_2 = ()=>{
                console.log("ctrl+w+enter unpressed!")
            }
            add_handler(
                [KeyNames.alt, KeyNames.w], // when alt+w are held ...
                KeyNames.Enter,             // and enter is ...
                false,                      // pressed, ...
                handler_1 ,                 // trigger the handler function `handler_1`.
            )
    
            add_handler(
                [KeyNames.alt, KeyNames.w], // when alt+w are held ...
                KeyNames.Enter,             // and enter is ...
                true,                       // released, ...
                handler_2 ,                 // trigger the handler function `handler_2`.
            )
    
            return ()=>{
                del_handler(
                    [KeyNames.alt, KeyNames.w], 
                    KeyNames.Enter, 
                    false, 
                    handler_1 , 
                )
                del_handler(
                    [KeyNames.alt, KeyNames.w], 
                    KeyNames.Enter, 
                    true, 
                    handler_2 , 
                )
            }
        },[])
    
        return holding ? <p>pressed ctrl + s</p> : <></>
    }

    When the second argument of add_handler (as well as del_handler) is an empty string, it listens to the reach of a certain key combination, no matter which key is the last to be pressed.

    For example, the following handler will be triggered either when alt is held and w is being pressed, or when w is held and alt is being pressed.

    add_handler([KeyNames.alt, KeyNames.w], "", false, handler)

    Similarly, the following handler will be triggered either when alt is held and w is being released, or when w is held and alt is being released.

    add_handler([KeyNames.alt, KeyNames.w], "", true, handler)

    You can also use "down" or "up" as the third argument.

    // these two lines has exactly the same behavior
    add_handler([KeyNames.alt, KeyNames.w], KeyNames.Enter, false, handler)
    add_handler([KeyNames.alt, KeyNames.w], KeyNames.Enter, "down", handler)

Space Navigator

The Space Navigator is a built-in plugin provided by mouseless that enables keyboard-based navigation across UI elements.

A navigator is defined as a graph (called space). Each node of a space is a string and each edge defines a directional transition between two nodes, triggered by a specific key.

Each space need to have a holding key combination. The space is activated only when this key combination is held.

Each edge requires a pressing key that initiates movement from one node to another. It will only take effect when the corresponding space is currently active (i.e. the holding keys are held).

import { SpaceDefinition } from "@ftyyy/mouseless"

const my_space: SpaceDefinition = {
    name        : "my_space",         // the unique name of the space
    nodes       : ["1", "2", "3"],    // the node list of the space 
    onStart     : (last_node)=> (last_node ?? "1"),   // the node to be activated when first enter the space

    // the key combinations that enters the space when held
    holding     : [KeyNames.alt, KeyNames.w],

    // transition rules between nodes.
    // trnaisiton will take effect when the `holding` keys of the space are pressed and `trigger` key of the egde is being pressed.
    edges       : [
        {
            pressing: KeyNames.ArrowRight, 
            onMove: (cur_node)=>(
                // return `"no_action"` means not triggering this edge.
                ({"1": "2", "2": "3", "3": "1"})[cur_node] ?? "_no_action"
            )
        } , 
        {
            pressing: KeyNames.ArrowLeft , 
            onMove: (cur_node)=>(
                ({"2": "1", "3": "2", "1": "3"})[cur_node] ?? "_no_action"
            )
        }
    ],
}

To apply a space, pass it to the KeyEventManager:

import { KeyEventManager } from "@ftyyy/mouseless"

function App(){
    return <KeyEventManager
        spaces = {[my_space]} // pass the space definition to `KeyEventManager` here!
        preventing_default = {[
            [KeyNames.alt, KeyNames.w], // surpass the default behavior 
        ]}
    >{(keydown_proxy, keyup_proxy) => (
        <div 
            tabIndex  = {0}
            onKeyDown = {keydown_proxy}
            onKeyUp   = {keyup_proxy}
        >
            <YourComponent />
        </div>
    )}</KeyEventManager>
}

Navigator Hooks

  • useSpaceNavigatorState: Returns the names of current activated space and node.
  • useSpaceNavigatorOnMoveRegister: Provides register/unregister functions for movement handlers. These functions will be called when a movement between nodes in the space is happening.
import * as React from "react"
import {
    useSpaceNavigatorState , 
    useSpaceNavigatorOnMoveRegister , 
} from "@ftyyy/mouseless"


import type {
    SpaceName , // (they are both strings actually. You can just use `string`) 
    NodeName , 
} from "@ftyyy/mouseless"

function YourComponent(){
    // get current activated space and node
    const [space, node] = useSpaceNavigatorState()

    // get registeration functions for moving events listeners
    const [add_move_handler, del_move_handler] = useSpaceNavigatoronMoveRegister()

    React.useEffect(()=>{

        // define a moving event handler
        const handler = (
            start_space ?: SpaceName, 
            start_node  ?: NodeName, 
            end_space   ?: SpaceName, 
            end_node    ?: NodeName , 
        )=>{
            if(start_space == end_space && start_space == "my_space"){
                set_words(`moved from ${start_node} to ${end_node}`)
            }
        }

        // register the handler
        add_move_handler(handler)
        return ()=>{
            // unregister the handler when unmount
            del_move_handler(handler)
        }
    },[])

    
    // if a node is current activated, give it a solid border.
    const make_styles = (node_name: string)=>{
        if(node == node_name){
            return {
                style: { border: "1px solid #000" }
            }
        }
        return {}
    }

    return (space == "my_space") ? (<div>
        <p {...make_styles("1")}>element 1</p>
        <p {...make_styles("2")}>element 2</p>
        <p {...make_styles("3")}>element 3</p>
    <div>) : <></>
}

Efficiency Optimization

useSpaceNavigatorState accepts two optional parameters: target_space: SpaceName and target_node: NodeName. With one of two parameters specified, useSpaceNavigatorState will only return results when the specified parameter is met, and otherwise return [undefined, undefined]. This allows a finer control over the rerender behavior of the React component.

import * as React from "react"
import {
    useSpaceNavigatorState , 
} from "@ftyyy/mouseless"

function YourComponent(){
    // only rerender when `space=="my_space"`
    const [space, node] = useSpaceNavigatorState("my_space")

    return (space == "my_space") ? <div>my space!</div> : <></>
}

mouseless also provides a lower-level hook useSpaceNavigatorRawState, allowing users to specify a selector. The selector can access to the internal state and return whatever inforamtion user requires. Use state.space and state.node to access current space and node.

import * as React from "react"
import {
    useSpaceNavigatorState , 
} from "@ftyyy/mouseless"

// a space shared by two components
const my_space: SpaceDefinition = {
    name        : "my_space", 

    // 2 components share the same space.
    // node format: `${component_idx},{node_idx}`.
    nodes       : [
        "0,0", "0,1", "0,2", 
        "1,0", "1,1", "1,2"
    ], 

    onStart     : (last_node)=> (last_node ?? "0,0"), 
    holding     : [KeyNames.alt, KeyNames.w],
    edges       : [
        {// switch component
            pressing: KeyNames.ArrowUp, 
            onMove: (cur_node)=>{
                const [component, idx] = cur_node.split(",")
                return `${parseInt(component) ^ 1},${idx}`
            }
        } , 
        {// switch component
            pressing: KeyNames.ArrowDown, 
            onMove: (cur_node)=>{
                const [component, idx] = cur_node.split(",")
                return `${parseInt(component) ^ 1},${idx}`
            }
        } , 
        {// switch idx
            pressing: KeyNames.ArrowLeft, 
            onMove: (cur_node)=>{
                const [component, idx] = cur_node.split(",")
                return `${component},${(parseInt(idx)+2)%3}`
            }
        } , 
        {// switch idx
            pressing: KeyNames.ArrowLeft, 
            onMove: (cur_node)=>{
                const [component, idx] = cur_node.split(",")
                return `${component},${(parseInt(idx)+1)%3}`
            }
        } , 
    ],
}

function YourComponent_1(){
    const [node] = useSpaceNavigatorRawState(React.useCallback(()=>{
        const {space, node} = state
        if(space != my_space.name){
            // when navigating in other spaces, the component would not rerender.
            return undefined 
        }
        const [component, idx] = node.split(",")
        if(components != "1"){
            // when switching in this spaces but the other component, this component would also not rerender.
            return undefined
        }
        return idx
    }, []))

    return <div>current node is {node}</div>
}

function YourComponent_2(){
    const [node] = useSpaceNavigatorRawState(React.useCallback(()=>{
        const {space, node} = state
        if(space != my_space.name){
            // when navigating in other spaces, the component would not rerender.
            return undefined 
        }
        const [component, idx] = node.split(",")
        if(components != "2"){
            // when switching in this spaces but the other component, this component would also not rerender.
            return undefined
        }
        return idx
    }, []))

    return <div>current node is {node}</div>
}

License

MIT License