Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Hand desktop

New

A playful desk you work with your hands: pinch to grab folders and hatch pixel critters, wave, or touch both hands to start a dance.

hand-desktop/hand-desktop.tsx
Live · interactive

Arrow keys move it; Enter opens it.

how to play
  • Pinch a folder or a bar to grab it, or the sky to hatch a critter.
  • Wave an open hand: the critters wave back.
  • Both hands, index tips touching: dance party.

The pointer is your hand: press to pinch, shake to wave. The camera runs on this device; nothing is uploaded.

Time now

↑ Time for some dancing
Stand up and shake it

Pointer hand

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A playful landing, a portfolio hero or an event booth where the point is to try hand gestures, with a pointer that works the same way when there is no camera.
  • Showing what on-device hand tracking can do (pinch, wave, two hands together) without sending a frame anywhere.
  • A toy desk to drag a few folders and windows around, with small creatures that react to what the hands do.
  • A kiosk or a stage screen where people stand back from the keyboard and use their hands in the air.

When not to use

  • A real file manager or desktop people work in. Use a list or grid of files with a proper context menu and keyboard selection.
  • Anything that must be fast and exact: gestures in the air are slow and tiring. Use plain buttons and drag and drop with a mouse.
  • A page where asking for the camera would surprise or worry the reader. Pass camera={false}, or use a static illustration.
  • A small card or a dense layout: the desk needs at least 440 px of height to read. Use a still image of it or a short video.
  • Teaching sign language or anything where a misread gesture has a cost: the tracker is tuned for fun, not accuracy.

You can add this component with the shadcn CLI or by hand:

Into components/hand-desktop of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/hand-desktop.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

playground.tsx
import { HandDesktop } from './hand-desktop';export function Playground() {  return <HandDesktop critters={4} onGesture={(gesture) => console.log(gesture)} />;}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

A white hand skeleton floats over a painted sky of rolling hills. Pinch a folder or a window bar to carry it, pinch the sky to hatch a pixel critter that drops onto a folder or the dock and wanders along it, wave an open hand and the critters wave back, and touch both index fingers for a dance party. The pointer is a virtual hand by default; Use camera swaps in your real hands, tracked on the device.

  • Every hand is 21 points in the desk’s pixels, wherever it comes from: the pointer’s virtual hand, the demo’s scripted hands or the camera’s tracked ones. One piece of code reads all of them, so the demo, the gallery and the camera behave alike.
  • The virtual hand blends three poses (open, pinching, pointing) and holds the gap between thumb and index tip under the pointer; pressing closes the pinch. Two touches are two hands.
  • Gestures are measured in palm lengths: a pinch closes under 0.28 and opens over 0.45, so jitter never fires twice; an open hand that swings left and right three times waves; two index tips within half a palm start the dance until they part by a whole palm.
  • A pinch grabs the critter, window bar or folder under it, or hatches a critter there. Critters pop, flutter, then fall onto the first surface below (a folder, the window, the clock, the dock or the grass), ride along when it moves and walk the dock and the grass.
  • Use camera fetches the hand tracker only then (its module, WebAssembly runtime and model, from a CDN by default) and asks for the camera after it. Landmarks are mirrored, the middle 80 % of the frame spans the desk, and a One Euro filter steadies each point. Frames never leave the browser; Stop camera or leaving ends every track.
  • One loop runs while the desk is on screen and its tab is visible: it reads the hands, moves the held parts by transforms and draws the critters (12 × 12 sprites stamped from cached canvases) and the skeletons on one canvas at the device pixel ratio.
  • The sky, the hills and the folders are SVG with light-dark() tokens, so the desk turns to dusk with stars on a dark page or with the colorScheme prop.
PropTypeDefaultDescription
autoplaybooleantrueWhile on screen and untouched, a scripted hand grabs a folder, hatches critters, waves and calls in a second hand for the dance. Any real input, or the camera, hands it to the reader.
hintstring'Pinch anywhere to hatch a critter!'What the pill at the top says when nothing is happening.
crittersnumber3How many critters already walk on the dock (up to 9).
camerabooleantrueShows the Use camera button. Without it, the desk is pointer-only.
sources{ module?: string; wasm?: string; model?: string }—Where the hand tracker’s ES module, WebAssembly folder and model file come from, to host them yourself. A public CDN by default; nothing loads until the reader presses Use camera.
onGesture(gesture: 'hatch' | 'grab' | 'drop' | 'wave' | 'dance') => void—Called when a gesture does something.
colorScheme'light' | 'dark' | 'system'—Day or dusk colours, or the reader’s; without it, the page’s color-scheme.
defaultState{ open?: 'guide' | 'sketches' | 'notes' | 'pets' | null; hand?: 'open' | 'grab' | 'hatch' | 'dance'; camera?: 'loading' | 'asking' | 'lost' | 'denied' | 'missing' | 'unsupported' | 'failed' }—Opens in a state, for stills and docs, with the demo off: the window open at first, a still hand (open over a folder, holding it, hatching a critter, or two hands dancing) and what the camera chip says. No camera is asked for.
classNamestring—Classes for the outer element.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

hand-desktop.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * Hand desktop: a playful desk you work with your hands. A white hand skeleton floats over a * painted sky; pinch on a folder or a window's bar to grab it and open your fingers to drop it; * pinch on the sky to hatch a pixel critter, which drops onto a folder, the clock or the dock and * wanders along it; wave an open hand and the critters wave back; touch both index fingers * together for a dance party. * * - The hand is your pointer by default: a virtual 21-point hand, the pinch under the pointer, *   that closes while you press. Two fingers on a touch screen are two hands. The autoplay demo *   and the gallery use the same hand, scripted. * - "Use camera" swaps in your real hands: only then is the hand tracker fetched (its module, *   runtime and model, from a CDN by default), and only then is the camera asked for. It runs on *   the device: no frame leaves the browser. Your feed shows mirrored and dimmed in a corner, only *   while the camera is on; "Stop camera", or leaving, ends it. * - engine.ts runs one loop while the desk is on screen: it reads every hand's gestures the same *   way (hand-model.ts), moves what is held, runs the critters and draws them and the skeletons *   on one canvas. The HTML parts only move by transforms. * - Folders are buttons (arrow keys move them, Enter opens), and the guide window and the dock have *   buttons for each gesture, so everything works without a hand at all. * - Light or dark: the colours are light-dark() tokens, so the sky turns to dusk with the page's
engine.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The desk's engine: one loop, once a frame, while the desk is on screen. It gathers the hands * (the reader's pointer hand, the demo's scripted hands, the camera's tracked hands), reads their * gestures, moves what they hold, runs the critters, and draws the critters and the skeletons on * one canvas over the desk. The folders, the window, the clock and the dock are HTML that it only * moves, by transforms. * * Every hand, wherever it comes from, is 21 points in the desk's px, so one piece of code reads * the gestures for all of them: a pinch grabs what is under it or hatches a critter, an open hand * waving left and right greets the critters, two index tips touching start a dance. */import {  BONES,  INDEX_TIP,  OneEuro,  PINCH_OFF,  PINCH_ON,  TOGETHER_OFF,  TOGETHER_ON,  WRIST,  cursorOf,
hand-model.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The hand, as 21 points: the wrist, then four joints for each of the thumb, index, middle, ring * and little finger, from the knuckle to the tip. A camera tracker gives these points; without * a camera, a virtual hand makes them from three poses (open, pinching, pointing) blended * together, so the gestures below read both the same way. * * Every measure is divided by the palm's length (wrist to the middle finger's knuckle), so a hand * near the camera and one far away read the same. */export type Point = { x: number; y: number };export type Landmarks = Point[];/** Index of each point, by name. */export const WRIST = 0;export const THUMB_TIP = 4;export const INDEX_PIP = 6;export const INDEX_TIP = 8;export const MIDDLE_MCP = 9;/** The bones the skeleton draws, as pairs of points. */export const BONES: [number, number][] = [
tracker.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The camera and the hand tracker. Nothing here runs until the reader asks for the camera: * the tracker's module, its WebAssembly runtime and its model are fetched then, at runtime, from * the addresses given (a CDN by default; a site can host the three files itself), never bundled. * * Frames stay in the browser: the model runs locally, and only its 21 points per hand come out. * Nothing is recorded, kept or sent. stop() ends every camera track. */import type { Landmarks } from './hand-model';export type TrackerSources = {  /** The tracker's ES module (it exports FilesetResolver and HandLandmarker). */  module: string;  /** The folder of its WebAssembly runtime. */  wasm: string;  /** The hand landmark model file. */  model: string;};export const DEFAULT_SOURCES: TrackerSources = {  module: 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/vision_bundle.mjs',
critters.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The critters: six pixel animals, 12 × 12, facing right, each with two walking frames (only the * feet change). A sprite is a list of rows, one letter per pixel ('.' is see-through), and a * palette for its letters. They are drawn once per pixel size into small canvases, then stamped. */export type Sprite = {  name: string;  palette: Record<string, string>;  /** Rows of the body, then the feet of each walking frame. */  body: string[];  feet: [string, string];};const INK = '#1b1b1f';export const CRITTERS: Sprite[] = [  {    name: 'chick',    palette: { k: '#4a3412', y: '#ffd84a', Y: '#efb21c', w: '#fff6c2', e: INK, o: '#ff8a2a' },    body: [      '............',
art.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * The desk's drawings, all decorative: the painted sky with its rolling hills (a dusk palette at * night, from the stylesheet's tokens), the folder icon, the dock's icons and a critter drawn in * SVG squares for the pets window and the dock. */import { useId, type ReactNode } from 'react';import { rows, type Sprite } from './critters';import styles from './hand-desktop.module.css';/** Soft clouds: groups of blurred ellipses, in the wallpaper's 1600 × 1000 units. */const CLOUDS: { x: number; y: number; s: number; puffs: [number, number, number, number][] }[] = [  {    x: 180,    y: 150,    s: 1,    puffs: [      [0, 0, 120, 34],      [70, -22, 70, 34],      [-60, -10, 60, 26],
use-autoplay.ts
'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(      ([entry]) => {        onScreen = entry.isIntersecting;
hand-desktop.module.css
/* Hand desktop (hand-desktop.tsx): a painted sky over rolling hills, with folders, a window,   a clock card, a frosted dock, a hint pill and the camera's status, and one canvas on top for the   critters and the hand skeletons.   Sizes follow the desk's width (cqw: a hundredth of it), clamped so text stays legible on a phone   and nothing grows past its natural size on a wide screen.   Colours are the tokens below, light-dark(day, dusk): the component takes the color-scheme it   inherits from the page, or the one its colorScheme prop sets (data-color-scheme). */.root {  /* The sky, top to horizon, and its clouds and stars. */  --sky-top: #1f5bd6;  --sky-mid: #4a8ff0;  --sky-low: #b4dafb;  --cloud: rgb(255 255 255 / 0.96);  --star: transparent;  /* The hills, far to near, top and foot of each. */  --far-top: #9fd2a2;  --far-low: #69ad68;  --mid-top: #86d447;  --mid-low: #2e7c1b;  --near-top: #4fa82c;  --near-low: #286915;
  • Everything a gesture does has a button too: folders are buttons (arrow keys move them, Shift for bigger steps, Enter opens them), the guide window has Hatch, Wave and Dance, and the dock can hatch a critter, open the guide or send the critters home.
  • A polite live region says what happened (a critter hatched, a folder moved, the dance started) and every camera state: loading, allow the camera, tracking, hand lost, blocked or unavailable.
  • The camera is never requested on its own: only after an explicit press of Use camera, which is a toggle button (aria-pressed). Frames are processed on the device and nothing is uploaded; the feed shows only to the reader, mirrored and dimmed.
  • The skeleton, the critters, the wallpaper and the hint pill are decorative and hidden from assistive technology; the real cursor comes back over the dock, the window body and the status buttons.
  • Under reduced motion there is no demo, critters stand still and land at once, and the clock does not wiggle.