Drop-in browser games for shadcn.
Minimal, themeable React/TSX games installable with the shadcn CLI — perfect for 404 pages, empty states, loading screens, and landing-page easter eggs.
Website · Games · Docs · Contributing · Report a bug
npx shadcn@latest add @gamekitui/snakeNo provider. No peer deps. No init step. Each game is a single drop-in file that inherits your shadcn theme automatically — light/dark and any preset. Drop one on a 404 and it plays on the first keypress.
- 🪶 Minimal — each game is a single file with zero npm dependencies beyond
reactand your existingcnhelper. 2–4 KB minified + gzipped (the raw.tsxis bigger because it inlines its engine and theme hooks), and lazy-loadable so it never weighs down your initial bundle. - 🎨 Themeable — your
--primary/--secondary/--accentdrive the playfield, not just the chrome. Canvas games read the tokens at runtime; DOM games use Tailwind token classes. Change your theme and the games recolor live. - 🚫 Zero assets — no images, audio, or fonts. Every pixel is drawn with CSS or
<canvas>. - ♿ Accessible — keyboard + touch input, visible focus rings,
aria-liveannouncements, andprefers-reduced-motionsupport in every game.
| Game | Surface | Install |
|---|---|---|
| 🐍 Snake | canvas | npx shadcn@latest add @gamekitui/snake |
| ⭕ Tic-Tac-Toe | DOM | npx shadcn@latest add @gamekitui/tic-tac-toe |
| 🔢 2048 | DOM | npx shadcn@latest add @gamekitui/2048 |
| 🃏 Memory Match | DOM | npx shadcn@latest add @gamekitui/memory-match |
| 🔨 Whack-a-Mole | DOM | npx shadcn@latest add @gamekitui/whack-a-mole |
| 💣 Minesweeper | DOM | npx shadcn@latest add @gamekitui/minesweeper |
| 🏓 Pong | canvas | npx shadcn@latest add @gamekitui/pong |
| 🧱 Breakout | canvas | npx shadcn@latest add @gamekitui/breakout |
| 🦖 Dino Runner | canvas | npx shadcn@latest add @gamekitui/dino-runner |
| 🐦 Flappy | canvas | npx shadcn@latest add @gamekitui/flappy |
Base registry URL:
https://gamekitui.com/r/{name}.json
// app/not-found.tsx
import { Snake } from "@/components/games/snake";
export default function NotFound() {
return (
<main className="grid min-h-svh place-items-center">
<div className="space-y-4 text-center">
<h1 className="text-4xl font-semibold">404</h1>
<p className="text-muted-foreground">Play a round while you decide where to go.</p>
<Snake className="mx-auto rounded-lg border" width={320} />
</div>
</main>
);
}That's it — no setup. On a single-game page like this, the game captures keyboard input globally, so it responds the moment a visitor presses a key (no click-to-focus needed).
The installed .tsx looks big (~15–28 KB) because it inlines its own engine and theme hooks to stay a true single-file drop-in — but that's source, not what ships. Built for production each game is 2–4 KB minified + gzipped.
Because every game is a self-contained module, it's trivially code-split — lazy-load it so it never touches your initial bundle and only downloads when the easter egg actually renders:
import dynamic from "next/dynamic";
const Snake = dynamic(() => import("@/components/games/snake").then((m) => m.Snake), {
ssr: false,
loading: () => <div className="aspect-square w-full animate-pulse rounded-lg bg-muted" />,
});@gamekitui/<game> installs resolve straight from the official shadcn registry directory — no setup needed. If you'd rather pin the registry in your project config:
npx shadcn@latest registry add @gamekituiOr add "@gamekitui": "https://gamekitui.com/r/{name}.json" to the registries field of your components.json by hand. You can also install any game by its direct URL: npx shadcn@latest add https://gamekitui.com/r/snake.json.
Every game accepts a common subset of props:
| Prop | Type | Description |
|---|---|---|
className |
string |
Tailwind classes on the wrapper. |
width / height |
number |
Logical size in CSS px (canvas games scale via DPR). |
paused |
boolean |
Externally pause the game. |
autoFocus |
boolean |
Focus on mount. Defaults to true. |
captureGlobalKeys |
boolean |
Listen for keys on window so the game works without being focused first. Defaults to true — set false when several games share a page, or the game sits in scrollable content, so it only responds while focused. |
persistHighScore |
boolean | string |
localStorage key, or a default per game. |
onScoreChange |
(score: number) => void |
Fires when the score changes. |
onGameOver |
(r: { score: number; won: boolean }) => void |
Fires on game over. |
apps/web Next.js 16 marketing + docs site; serves /r/*.json
packages/registry The game sources (one self-contained file each)
packages/game-core Shared contract types + reference hooks (zero runtime)
packages/ui shadcn primitives used by the site
scripts/build-registry.ts Emits apps/web/public/r/*.json from the game sources
bun install
bun run dev # builds the registry, then starts the site on :3001Other scripts:
bun run build # registry build + next build
bun run check-types # typecheck every workspace
cd packages/registry && bun smoke.tsx # render smoke test for every gameContributions are welcome! See CONTRIBUTING.md for the "add a new game" guide — the engine/wrapper template, the shared props contract, the theme-token mapping, and the size budget. Please also review our Code of Conduct.
Found a bug or have an idea? Open an issue.
GameKit UI is an independent, unaffiliated community project. It is not built, sponsored, or endorsed by the shadcn/ui team. It uses the shadcn registry system. The shadcn- prefix is reserved for official projects, which is why this project is named gamekitui (not shadcn-games).
MIT © GameKit UI contributors