Draw pixel art together, in real time. Create a room, share the link, and make something with your friends. No sign-up, no install, free forever.
PixelParty is a real-time collaborative pixel art canvas. Open the page, agree to the terms, create a room, share the URL, and draw together live — seeing each other's pixels and cursors appear as you go.
V1 · Jeffrey Hamilton · Donate
- Land on the page → agree to Terms & Privacy → start. A one-time consent
gate (stored in
localStorage) covers the no-data, no-tracking promise. - Unique room URLs like
/?room=ABC123— share the link, friends join and draw together in real time. - Live cursors & pixels from everyone in the room.
- Tools: pencil, line, rectangle (with drag preview), fill bucket, eraser, eyedropper.
- Full color picker — any color via the native picker or hex entry.
- Adjustable canvas: 16×16, 32×32, 64×64 (selector in the toolbar).
- Clear canvas button in the toolbar.
- Export PNG of the finished art.
- Theme-aware — light/dark mode changes the whole UI including the canvas area (no flat dark box in light mode).
- Rooms are ephemeral — state lives in server memory and is garbage-collected after 24h of inactivity. No accounts, no stored user data, no tracking.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router) + TypeScript |
| Styling | Tailwind CSS 4 + shadcn/ui (New York) |
| Real-time | socket.io (see Architecture below) |
| State | React refs + requestAnimationFrame (hot path bypasses React state) |
| Icons | lucide-react |
| Package manager | bun |
Browser A ◄──WebSocket──► socket.io server (port 3004) ◄──WebSocket──► Browser B
(ephemeral in-memory
per-room canvas state)
- One socket.io room per PixelParty room (room ID = the URL code).
- Canvas state = a flat array of hex colors (or
null), held in server memory and broadcast as diffs. - On join: the server sends the full current canvas (
sync). - On pixel place: the client sends a batched
place; the server updates state and broadcasts only the diff to others. - 24h GC of inactive rooms.
The wire protocol (event names + payloads) is defined once in
src/lib/pixel-party/constants.ts and shared
by the frontend and the server, so the real-time layer is portable: in this
sandbox it's a socket.io mini-service reachable through a Caddy gateway
(?XTransformPort=3004); in production you can deploy the same server anywhere
or port the handlers to PartyKit with no client changes.
src/
app/
layout.tsx # metadata, favicon, ThemeProvider (dark default)
page.tsx # Suspense + <PixelPartyApp/> router
components/
pixel-party/
pixel-party-app.tsx # terms gate + routes Landing vs Room by ?room=
terms-modal.tsx # Terms & Privacy gate (gate mode) / viewer (view mode)
landing.tsx # create / join screen
room.tsx # header + sidebar/bottom bar + canvas + footer
header.tsx # logo, room code, player count, export, share, settings
pixel-canvas.tsx # canvas (rAF rendering, 6 tools, shape preview, cursors)
toolbar.tsx # pencil / line / rectangle / fill / eraser / eyedropper
color-picker.tsx # full native color picker + hex input
size-selector.tsx # 16 / 32 / 64 segmented control
room-code.tsx # click-to-copy room code
share-button.tsx # copies the full room URL
settings-dropdown.tsx # theme toggle, donate, privacy, terms
footer.tsx # centered "V1 · Jeffrey Hamilton"
logo.tsx # flat SVG pixel-grid mark
theme-provider.tsx # next-themes wrapper
hooks/
use-pixel-room.ts # socket.io connection + in-memory pixel state (refs)
lib/
pixel-party/
constants.ts # protocol types & event names (shared), room-code gen
mini-services/
pixel-server/
index.ts # socket.io real-time server (port 3004)
package.json
public/
favicon.svg # pixel-grid icon
# 1. Install dependencies
bun install
# 2. Start the real-time server (port 3004)
cd mini-services/pixel-server
setsid --fork bun index.ts # detach so it survives the shell exiting
cd ../..
# 3. Start the Next.js app (port 3000)
bun run devOpen the app through the gateway at http://localhost:81/ (Caddy routes
?XTransformPort=3004 → the real-time server, everything else → Next.js).
The mini-service must be started with
setsid --fork(or equivalent) so it reparents to init and survives across shell calls; plain&backgrounding gets reaped when the spawning shell exits.
git init
git add .
git commit -m "PixelParty v1"
# create an empty repo on GitHub, then:
git remote add origin https://github.com/<you>/pixelparty.git
git branch -M main
git push -u origin main.gitignore is already configured (ignores node_modules, .next, *.log,
.env*, .vercel, db/*.db, etc.).
PartyKit runs on Cloudflare's edge — one party instance per room, in-memory state, no server to manage. Free tier stays warm (unlike Render).
npm install -g partykit # or: npx partykit
npx partykit deploy # from the repo rootThis deploys party/index.ts and gives you a URL like:
pixelparty.<your-name>.partykit.dev
First time only:
npx partykit deployopens a browser to log you into PartyKit (GitHub/Google). After that, deploys are instant.
- Import the repo at vercel.com/new.
- Framework preset: Next.js. No build overrides —
buildisnext build. - Add the environment variable:
NEXT_PUBLIC_PARTYKIT_HOST=pixelparty.<your-name>.partykit.dev(your PartyKit host, nohttps://prefix, no trailing slash)
- Deploy. The client connects to PartyKit for real-time sync.
- Open your Vercel URL → create a room → draw
- Copy the share link, open in a second browser/incognito
- Both player counts should show 2, pixels appear on both screens in real time
The app still works — it falls back to solo mode (local-only: draw, undo, save to gallery, export PNG all work; just no multiplayer). An amber banner tells you. So you can deploy to Vercel first, add PartyKit later.
The repo also includes mini-services/pixel-server/ (a socket.io server) for
the sandbox. If you prefer socket.io over PartyKit for production, deploy it to
Render/Railway/Fly and set NEXT_PUBLIC_REALTIME_URL instead. The wire protocol
is identical — both servers implement the same events from constants.ts.
- Dirty-cell rendering: the canvas redraws only changed cells each frame via
a
requestAnimationFrameloop — never a full repaint per pixel. - Refs over state on the hot path: pixel and cursor updates mutate refs and drive the canvas directly; only low-frequency events (join/leave/size/connect) touch React state.
- Shape previews (line/rectangle) render on a dedicated overlay canvas, committed only on pointer-up.
- Batched sends: rapid placements are buffered and flushed every 50ms.
- Mobile: canvas is width-constrained;
touch-action: nonekeeps drawing smooth; bottom bar holds picker + size + clear + scrollable tools.
Jeffrey Hamilton — Donate
PixelParty stores no user data and tracks nothing. Rooms expire after 24h of inactivity.