Installation

Install GameKit UI games with the shadcn CLI — by registry URL or the @gamekitui namespace. Includes the usage pattern and the shared props every game accepts.

Install a game

GameKit UI is listed in the official shadcn registry directory, so install any game by short name — the CLI resolves the @gamekitui namespace automatically, no setup required:

npx shadcn@latest add @gamekitui/snake

Replace snake with any game (tic-tac-toe, 2048, flappy, …). The CLI writes the component into your project and rewrites the cn import to your own @/lib/utils — the only assumption is the standard shadcn helper.

You can also install by direct registry URL if you prefer:

npx shadcn@latest add https://gamekitui.com/r/snake.json

Pin the registry (optional)

@gamekitui installs work straight from the directory, but you can pin the registry in your project config:

npx shadcn@latest registry add @gamekitui

Or add it to components.json by hand:

{
  "registries": {
    "@gamekitui": "https://gamekitui.com/r/{name}.json"
  }
}

Use it

// 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>
  );
}

Bundle size & lazy-loading

The installed .tsx looks large (~15–28 KB) because each game inlines its own engine and theme hooks to stay a true single-file drop-in — but that's the source, not what ships. Built for production, every game is 2–4 KB minified + gzipped.

Each game is a self-contained module, so it's trivially code-split. Lazy-load it and it never touches your initial bundle — it 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" />,
});

Shared props

Every game accepts a common subset of props:

PropTypeDescription
classNamestringTailwind classes on the wrapper.
width / heightnumberLogical size in CSS px (canvas games scale via DPR).
pausedbooleanExternally pause the game.
autoFocusbooleanFocus on mount. Defaults to true.
captureGlobalKeysbooleanListen for keys on window so the game works without being focused first. Defaults to true — set false when several games share a page, or it sits in scrollable content, so it only responds while focused.
persistHighScoreboolean | stringlocalStorage key, or a default per game.
onScoreChange(score: number) => voidFires when the score changes.
onGameOver(r: { score: number; won: boolean }) => voidFires on game over.