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

arcade-shelf

v0.2.2

Published

Drop-in mini canvas games as a sidebar widget — a shelf for your mini games.

Readme

Your Arcade Shelf

npm license

Live demo

Drop-in mini canvas games as a sidebar widget — a shelf for your mini games, or canvases.

Not a game engine, not a canvas library. Just a small runtime that takes a list of self-contained canvas games and renders them as a clickable widget.

Install

npm install arcade-shelf

Quick start (ES modules)

import { createShelf, pong, minesweeper, snake } from 'arcade-shelf';
import 'arcade-shelf/style.css';

const shelf = createShelf({
  container: '#games',
  title: "Let's play!",
});
shelf.register(pong);
shelf.register(minesweeper);
shelf.register(snake);
shelf.mount();
<div id="games"></div>

Only the games you import end up in your bundle — the built-in games are tree-shakeable. Drop any you don't need from the import list.

Quick start (plain HTML, no build step)

<link rel="stylesheet" href="https://unpkg.com/arcade-shelf/dist/style.css">
<div id="games"></div>
<script src="https://unpkg.com/arcade-shelf/dist/index.umd.cjs"></script>
<script>
  const shelf = ArcadeShelf.createShelf({
    container: '#games',
    title: "Let's play!",
  });
  shelf.register(ArcadeShelf.pong);
  shelf.register(ArcadeShelf.minesweeper);
  shelf.register(ArcadeShelf.snake);
  shelf.mount();
</script>

Built-in games

Three reference implementations ship in the box. Pick whichever input paradigm is closest to the game you want to build, then copy the shape — each one handles start() / stop() listener lifecycle correctly.

| Game | Input paradigm | What to crib from it | |---------------|------------------------|-----------------------| | Pong 🎾 | Mouse / touch drag | mousemove + touchmove to track paddle position; getBoundingClientRect() cached on start() + resize instead of per-event; deltaTime normalization so motion is frame-rate independent. | | Minesweeper 💣 | Left / right click + long-press | click to reveal, contextmenu to flag, touchstart long-press as the touch equivalent of right-click (with swallow of the trailing click). | | Snake 🐍 | Keyboard + swipe | keydown on window, swipe on the canvas — two separate layers from the modal's document-level focus trap, so there's no ordering contention to reason about; touchstarttouchend diff for swipe direction; touchmove with passive: false to suppress page scroll while the finger is on the play area. |

All three are ~200–600 lines of pure canvas + DOM — no framework, no dependencies. Read them as recipes, not as a library surface. They're the shortest path from "I want to write a mini canvas game" to "it handles input and teardown correctly on both desktop and mobile."

Integration recipes

React

import { useEffect, useRef } from 'react';
import { createShelf, pong } from 'arcade-shelf';
import 'arcade-shelf/style.css';

export function GameShelf() {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!ref.current) return;
    const shelf = createShelf({ container: ref.current }).register(pong).mount();
    return () => shelf.unmount();
  }, []);
  return <div ref={ref} />;
}

Key detail: unmount() in the cleanup — otherwise HMR / route changes leak listeners and an open modal's RAF keeps running.

Hexo / Jekyll (no build step)

Drop the UMD bundle into your theme's static assets and reference it from the layout. Example for Hexo (similar for Jekyll _includes or 11ty includes):

<!-- themes/your-theme/layout/_widget/games.ejs -->
<link rel="stylesheet" href="https://unpkg.com/arcade-shelf/dist/style.css">
<div id="arcade-shelf-mount"></div>
<script src="https://unpkg.com/arcade-shelf/dist/index.umd.cjs"></script>
<script>
  ArcadeShelf.createShelf({ container: '#arcade-shelf-mount' })
    .register(ArcadeShelf.pong)
    .mount();
</script>

If you'd rather self-host (air-gapped builds, offline dev), copy node_modules/arcade-shelf/dist/ into your theme's assets folder and point the tags at the local paths.

Your own game

You do not need to fork arcade-shelf to add a game. shelf.register() accepts any object matching the Game contract, no matter where it's defined:

import { createShelf, type Game } from 'arcade-shelf';
import 'arcade-shelf/style.css';

const tetris: Game = {
  id: 'tetris',
  name: 'Tetris',
  icon: '🧱',
  canvasSize: { width: 320, height: 480 },
  init(canvas) {
    // ... your game logic
    return {
      start() { /* attach listeners, start RAF */ },
      stop()  { /* cancel RAF, remove listeners */ },
    };
  },
};

createShelf({ container: '#games' })
  .register(tetris)
  .mount();

See src/games/pong.ts for a worked example including dt normalization and cleanup, or src/games/snake.ts for keyboard input + fixed-step simulation.

The game contract

Every game implements this interface:

interface Game {
  id?: string;                                     // stable key (defaults to name)
  name: string;                                    // display label
  icon?: string;                                   // emoji or short string
  description?: string;
  order?: number;                                  // display order
  canvasSize?: { width: number; height: number };  // defaults to 380×280
  init(canvas: HTMLCanvasElement): GameHandle;
}

interface GameHandle {
  start(): void;
  stop(): void;                                    // cancel RAF, remove listeners, etc.
  pause?(): void;                                  // optional: tab hidden — suspend RAF/timers
  resume?(): void;                                 // optional: tab visible — restart them
  resize?(width: number, height: number): void;   // optional: canvas display size may have changed
}

That's the whole API contract. Write your game inside init, return start/stop, register it on a shelf, done.

id vs name

id is the stable registration key — what whitelist and order match against, and what the registry uses to dedup. name is the display label shown on the button. When id is omitted it defaults to name, so the simple case stays simple. Set id explicitly when you want to be able to rename name later (e.g. for translation) without breaking caller config.

init is called every time the modal opens

Closing the modal calls stop() and discards the canvas. Reopening the game calls init(canvas) again on a fresh canvas — any state held in the init closure (score, position, RNG state) starts over. If you need cross-session state, persist it externally (e.g. localStorage).

Optional pause / resume / resize hooks

The modal forwards three lifecycle events to your game so you don't have to register listeners yourself:

  • pause / resume fire on document.visibilitychange. Cancel your RAF and clear interval timers in pause; re-arm them in resume. The built-in games implement these — a Snake left in a backgrounded tab won't burn battery, and Minesweeper's wall-clock timer won't tick while you're away.
  • resize fires on window.resize. The canvas's pixel buffer (canvas.width / canvas.height) does not change — only its CSS display size might. Re-cache anything that depends on getBoundingClientRect() (e.g. input → game coordinate translation).

All three are optional: a game that doesn't implement them simply keeps running.

Beyond games

The runtime contract is just init(canvas) → { start, stop, pause?, resume?, resize? } — nothing in it is game-specific. Anything you'd put on a <canvas> fits on the shelf: interactive data viz, shader playgrounds, generative-art sketches, small tools. The built-ins are games because that's the project's vibe, not because the contract requires it. Override --arcade-shelf-canvas-bg if the default black backdrop clashes with your widget.

Multiple shelves on one page

Safe — the modal's body scroll lock is ref-counted, so two shelves can each open and close their modal without leaking the host page's overflow state.

createShelf({ container: '#shelf-arcade'  }).register(pong).mount();
createShelf({ container: '#shelf-widgets' }).register(myChart).mount();

License

MIT