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

@benev/tact

v0.2.0-0

Published

keybindings and gamepad support for web games

Readme

🎮 @benev/tact

web game input library, from keypress to couch co-op

npm install @benev/tact

tact is a toolkit for handling user inputs on the web.
it's good at user-customizable keybindings, multiple gamepad support, and mobile ui.

  • 🛹 #deck full setup with localstorage persistence
  • 🎮 #devices produce user input samples
  • 🧩 #bindings describe how actions interpret samples
  • 🔌 #port updates actions by interpreting samples
  • 🛞 #hub plugs devices into ports (multi-gamepad couch co-op!)
  • 📱 #nubs is mobile ui virtual gamepad stuff

🍋 tact deck

full setup with ui, batteries included

the deck ties together all the important pieces of tact into a single user experience, complete with ui views.

tact's ui is built with @e280/sly shadow views.

🛹 deck setup

  • import stuff from tact
    import * as tact from "@benev/tact"
  • setup your deck, and your game's bindings
    const deck = await tact.Deck.load({
    
      // how many player ports are possible? 1 is fine..
      portCount: 4,
    
      // where to store the user-customized bindings
      kv: tact.localStorageKv(),
    
      // default archetypal bindings for your game
      bindings: {
        walking: {forward: "KeyW", jump: "Space"},
        gunning: {
          shoot: ["or", "pointer.button.left", "gamepad.trigger.right"],
        },
      },
    })

🛹 plug devices into the hub

  • plug a keyboard/mouse player into the hub
    deck.hub.plug(new tact.PrimaryDevice())
  • automatically detect and plug gamepads
    tact.autoGamepads(deck.hub.plug)

🛹 do your gameplay

  • poll the deck, interrogate actions
    myGameLoop(() => {
    
      // do your polling
      const [p1, p2, p3, p4] = deck.hub.poll()
    
      // check if the first player is pressing "forward" action
      p1.actions.walking.forward.pressed // true
    
      // check how hard the second player is pulling that trigger
      p2.actions.gunning.shoot.value // 0.123
    })

🛹 deck ui: the overlay

  • render the deck overlay with sly
    import {dom} from "@e280/sly"
    
    dom.render(
      dom("#game-ui"),
      tact.DeckOverlay(deck),
    )
  • place the ui on top of your game canvas
    <div id="game-ui"></div>

🍋 tact devices

sources of user input "samples"

🎮 polling is good, actually

  • tact operates on the basis of polling
  • "but polling is bad" says you — but no — you're wrong — polling is unironically based, and you should do it
  • the gift of polling is total control over when inputs are processed, this is good for games
  • i will elaborate no further 🗿

🎮 basically how a device works

  • make a device
    const keyboard = new tact.KeyboardDevice()
  • reading samples looks like this
    for (const sample of keyboard.samples())
      console.log(sample) // ["KeyA", 1]
  • some devices have disposers to call when you're done with them
    keyboard.dispose()

🎮 samples explained

  • a sample is a raw input tuple of type [code: string, value: number]
  • a sample has a code string
    • it's either a standard keycode, like KeyA
    • or it's something we made up, like pointer.button.left or gamepad.trigger.right
  • a sample has a value number
    • 0 means "nothing is going on"
    • 1 means "pressed"
    • we don't like negative numbers
    • values between 0 and 1, like 0.123, are how triggers and thumbsticks express themselves
    • sometimes we use numbers greater then 1, like for dots of pointer movement like in pointer.move.up
    • don't worry about sensitivity, deadzones, values like 0.00001 — actions will account for all that using bindings later on

🎮 sample code reference

  • KeyboardDevice
  • PointerDevice
    • mouse buttons
      • pointer.button.left
      • pointer.button.right
      • pointer.button.middle
      • pointer.button.4
      • pointer.button.5
    • mouse wheel
      • pointer.wheel.up
      • pointer.wheel.down
      • pointer.wheel.left
      • pointer.wheel.right
    • mouse movements
      • pointer.move.up
      • pointer.move.down
      • pointer.move.left
      • pointer.move.right
  • GamepadDevice
    • gamepad buttons
      • gamepad.a
      • gamepad.b
      • gamepad.x
      • gamepad.y
      • gamepad.bumper.left
      • gamepad.bumper.right
      • gamepad.trigger.left
      • gamepad.trigger.right
      • gamepad.alpha
      • gamepad.beta
      • gamepad.stick.left.click
      • gamepad.stick.right.click
      • gamepad.up
      • gamepad.down
      • gamepad.left
      • gamepad.right
      • gamepad.gamma
    • gamepad sticks
      • gamepad.stick.left.up
      • gamepad.stick.left.down
      • gamepad.stick.left.left
      • gamepad.stick.left.right
      • gamepad.stick.right.up
      • gamepad.stick.right.down
      • gamepad.stick.right.left
      • gamepad.stick.right.right

🍋 tact bindings

keybindings! they describe how actions interpret samples

🧩 bindings example

  • let's start with a small example:
    const bindings = tact.asBindings({
      walking: {forward: "KeyW", jump: "Space"},
      gunning: {
        shoot: ["or", "pointer.button.left", "gamepad.trigger.right"],
      },
    })
    • walking and gunning are modes
    • forward, jump, and shoot are actions
    • note that whole modes can be enabled or disabled during gameplay

🧩 bindings are a lispy domain-specific-language

  • you can do complex stuff
    ["or",
      "KeyQ",
      ["and",
        "KeyA",
        "KeyD",
        ["not", "KeyS"],
      ],
    ]
    • press Q, or
    • press A + D, while not pressing S
  • you can get really weird
    ["cond",
      ["code", "gamepad.trigger.right", {range: [0, 0.5], timing: ["tap"]}],
      ["and", "gamepad.bumper.left", ["not", "gamepad.trigger.left"]],
    ]
    • hold LB and tap RT halfway while not holding LT

🧩 bindings atom reference

  • string — strings are interpreted as "code" atoms with default settings
  • "code" — allows you to customize the settings
    ["code", "KeyA", {
    	  scale: 1,
    	  invert: false,
    	  timing: ["direct"],
    }]
    • defaults shown
    • scale is sensitivity, the value gets multiplied by this
    • invert will invert a value by subtracting it from 1
    • clamp clamps the value with a lower and upper bound
    • range restricts value to the given range, and remaps that range 0 to 1
    • bottom zeroes the value if it's less than the given bottom value
    • top clamps the value to an upper bound
    • timing lets you specify special timing considerations
      • ["direct"] ignores timing considerations
      • ["tap", 250] only fires for taps under 250ms
      • ["hold", 250] only fires for holds over 250ms
  • "or" — resolves to the maximum value
    ["or", "KeyA", "KeyB", "KeyC"]
  • "and" — resolves to the minimum value
    ["and", "KeyA", "KeyB", "KeyC"]
  • "not" — resolves to the opposite effect
    ["not", "KeyA"]
  • "cond" — conditional situation (example for modifiers shown)
    ["cond", "KeyA", ["and",
      ["or", "ControlLeft", "ControlRight"],
      ["not", ["or", "AltLeft", "AltRight"]],
      ["not", ["or", "MetaLeft", "MetaRight"]],
      ["not", ["or", "ShiftLeft", "ShiftRight"]],
    ]]
    • KeyA is the value that gets used
    • but only if the following condition passes
  • "mods" — macro for modifiers
    ["mods", "KeyA", {ctrl: true}]
    • equivalent to the "cond" example above
    • ctrl, alt, meta, shift are available

🍋 tact port

polling gives you "actions"

a port represents a single playable port, and you poll it each frame to resolve actions for you to read.

🔌 port setup

  • make a port
    const port = new tact.Port(bindings)
  • attach some devices to the port
    port.devices
      .add(new tact.KeyboardDevice())
      .add(new tact.PointerDevice())
      .add(new tact.VpadDevice())
    • you can add/delete devices from the set any time
  • manipulate modes
    port.modes.clear()
    port.modes.add("walking")
    • actions only happen for enabled modes
    • you can toggle modes on and off by adding/deleting them from the modes set
  • you can update the bindings any time
    port.bindings = freshBindings
  • wire up gamepad auto connect/disconnect
    tact.autoGamepads(device => {
      port.devices.add(device)
      return () => port.devices.delete(device)
    })

🔌 interrogating actions

  • poll the port every frame
    port.poll()
  • now you can inspect the actions
    port.actions.walking.forward.value // 1
    • walking is a mode
    • forward is an action
    • action.value — current value
    • action.previous — last frame's value
    • action.changed — true if value and previous are different
    • action.pressed — true if the value > 0
    • action.down — true for one frame when the key goes from up to down
    • action.up — true for one frame when the key goes from down to up

🍋 tact hub

multiple gamepads! couch co-op is so back

you know the way old-timey game consoles had four controller ports on the front?

the hub embraces that analogy, helping you coordinate the plugging and unplugging of virtual controller devices into its virtual ports.

🛞 create a hub with ports

  • make hub with multiple ports at the ready
    const hub = new tact.Hub([
      new tact.Port(bindings),
      new tact.Port(bindings),
      new tact.Port(bindings),
      new tact.Port(bindings),
    ])
    • yes that's right — each player port gets its own bindings 🤯

🛞 plug in some devices

  • let's plug in the keyboard/mouse player
    hub.plug(new tact.PrimaryDevice())
    • the hub requires a single device to represent a player, so you can use a GroupDevice to combine multple devices into one
  • wire up gamepad auto connect/disconnect
    tact.autoGamepads(hub.plug)

🛞 now we're gaming

  • do your polling, interrogate those actions
    const [p1, p2, p3, p4] = hub.poll()
    
    p1.actions.walking.jump.value // 1
    p2.actions.walking.jump.value // 0

🍋 tact nubs

mobile ui like virtual thumbsticks and buttons

📱 nubs setup

  • render nub views with sly
    import {dom} from "@e280/sly"
    
    const stick = new tact.StickDevice()
    
    dom.render(
      dom("#controls"),
      tact.NubStick(stick),
    )

📱 nub stick

  • place a mount point onto your page
    <div id="controls"></div>
  • make the stick device yourself, then plug it into your hub or whatever
    const stick = new tact.StickDevice()
    
    dom.render(
      dom("#controls"),
      tact.NubStick(stick),
    )
    
    deck.hub.plug(stick)

🍋 tact is by https://benevolent.games/

building the future of web games