Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Pixel pedometer

New

A step counter with a pixel cat to look after: big pixel digits, a goal bar, hearts and a walk button.

pixel-pedometer/pixel-pedometer.tsx
Live · interactive

Today

Goal 8,000 steps, by tmrw 19:30

1,320 steps

Bean, kid, day 1.
16,273 of 50,000 XP, 33,727 to adult.

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 daily-goal screen on a phone: steps, water, minutes of practice, with one big number.
  • A habit app where a pet or mascot gives the goal a reason, and petting it is a small reward.
  • A retro or game-like brand, where pixel type is the voice and no font files should load.
  • One goal and one action on the screen: today’s count, its bar and a single “do it” button.

When not to use

  • Health data people read closely (heart rate, sleep stages). Use a plain chart with real type and axes.
  • Several metrics side by side. Use a dashboard of cards, each with its own number.
  • Text that changes language or length freely: the pixel fonts cover A–Z, digits and a few signs.
  • A sober or clinical product. The pet and its hearts read as a game; use a progress ring and a list.
  • A desktop page. It is drawn as a 390-point phone screen; on a wide layout use a stat card.

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

Into components/pixel-pedometer of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/pixel-pedometer.json

Either way it needs React 19.2 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

today-screen.tsx
import { PixelPedometer } from './pixel-pedometer';export function TodayScreen() {  return (    <PixelPedometer      steps={1320}      goal={8000}      deadline="By tmrw 19:30"      petName="Bean"      xp={16273}      xpGoal={50000}      autoplay={false}    />  );}

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 phone screen in a pixel-art style. Today’s steps in large pixel digits fill a lime bar towards the goal, and a little cat sits on a dotted line below. Tap or rub the cat and it shuts its eyes and smiles, a heart rises and it earns experience; press Walk together and it steps along while the count runs up. Every letter is drawn on a pixel grid, with no font files.

  • Every word and number is a pixel font written as rows of characters: four of them, a block face for the count (6 by 10), a lighter one for the goal and the percentage, a heading face and a 5 by 7 monospace for the small print. A line of text becomes one SVG path, a rectangle per run of lit pixels, scaled by its viewBox to its size in points; the real text sits beside it for screen readers.
  • The phone is designed in points, as a phone screen is: --pt is one point in px, the largest that fits the stage (0.82 px at most), so the 390 by 844 screen scales down as one picture on narrow stages.
  • The cat is a 48 by 52 sprite in two-pixel lines. Its head is a group of its own that drops a pixel and back every 450 ms, on steps(), so it breathes over a still body. Petted, the open eyes and the little w of a mouth swap for shut eyes and a mouth turned up, and the head nuzzles faster.
  • A tap pets it once; a press that rubs keeps it pleased and counts another pet every 650 ms. Each pet adds 5 XP, fills an empty heart and sends a small heart up in nine steps.
  • The walk button stands on a 3-point ledge of its own edge colour and presses down into it. Walking, the steps come in every 260 ms, the front legs lift in turn, the shoe stomps and the bonus blinks.
  • The experience track is two repeating gradients in 11.7-point slots: blocks up to a marker and dots after it, with the stage’s name under the marker.
  • While on screen and untouched, a demo plays: the day’s steps count up from zero and the bar fills, steps keep ticking in, a heart empties, then a finger pets and rubs the cat, starts a walk and stops it. Any real input hands it to the reader; it stops off screen and under reduced motion.
PropTypeDefaultDescription
stepsnumber1320Today’s steps so far.
goalnumber8000The day’s goal, in steps.
deadlinestring'By tmrw 19:30'When the goal is due, written under it.
petNamestring'Bean'The pet’s name.
stagestring'Kid'The pet’s stage of life, under its name and under the marker.
daynumber1The day of that stage.
nextStagestring'Adult'The stage the experience leads to.
xpnumber16273The pet’s experience.
xpGoalnumber50000The experience the next stage needs.
heartsnumber3Full hearts, out of three.
onPet(xp: number) => void—Called on each pet, with the experience the pet now has.
onWalkChange(walking: boolean) => void—Called when a walk starts or stops.
defaultWalkingboolean—Opens walking, the steps ticking in fast. Setting it turns the demo off.
defaultMood'calm' | 'happy'—Opens with the cat calm or pleased, held until it is petted. Setting it turns the demo off.
autoplaybooleantrueWhile on screen and untouched, the steps tick in and a finger pets the cat and goes for a walk.
colorScheme'light' | 'dark' | 'system'—Light or dark colours, or the reader’s; without it, the page’s color-scheme.
classNamestring—A class for the stage.

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

pixel-pedometer.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useCallback,  useEffect,  useEffectEvent,  useMemo,  useRef,  useState,  type CSSProperties,  type PointerEvent as ReactPointerEvent,} from 'react';import {  BLOCK,  CAT,  HEADING,  HEART,  HEART_OUTLINE,  LOCK,  MEDIUM,  MONO,  SHOE,
pixel-font.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The pedometer's type and art, drawn on pixel grids: each glyph or sprite is a list of rows, '#' * for a lit pixel. `toPath` turns rows into one SVG path (a rectangle per run of lit pixels), so * a whole line of text is a single path in a single SVG, scaled to any size by its viewBox. * * - Block: the step count's face, 6 by 10 with strokes two pixels thick and corners cut by one. * - Medium: the goal and the percentage, 8 by 13, lighter, with a narrow one. * - Heading: the day's title, 7 by 10. * - Mono: the small print, a monospaced 5 by 7 with one-pixel strokes. */export type PixelFont = {  /** Rows above the baseline; glyphs may hang below it (a comma's tail). */  height: number;  /** Pixels between two glyphs (fractions allowed). */  gap: number;  /** Each glyph's width in pixels, when the font is monospaced. */  mono?: number;  glyphs: Record<string, string[]>;};/** One SVG path for a grid of rows, offset by `x`, `y` pixels. */
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'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. `visible` and `reduced` are there for what * runs on its own whoever is in charge. */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);
pixel-pedometer.module.css
/* Pixel pedometer (pixel-pedometer.tsx): a graphite phone on a quiet stage, its screen an   off-white page of black pixel type with a lime bar and a lime button; at night the page is   near-black and the type light, the lime the same. The phone is drawn in points, as a phone's   screen is designed: --pt is one point in px (0.82 at most, less on a stage too narrow for it),   so the 390 by 844 screen scales as one picture. Every word is an SVG path (pixel-font.ts),   sized in points here or inline.   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes   the color-scheme it inherits from the page (light unless the page says otherwise), or the one   its colorScheme prop sets (data-color-scheme).   Memory. The phone is one layout boundary of a fixed size. What moves moves by transforms: the   cat's head (a group in its SVG, a pixel up or down), the bar's fill (scaleX), the button (its   press), the hearts that rise and the demo's finger. The numbers redraw as paths. */.root {  /* The stage and its edge. */  --surface: #ebeae6;  --line: rgb(0 0 0 / 6%);  /* The phone: its frame (two stops of its sheen and its edge), the buttons, its drop shadow. */  --frame-light: #4a4a4e;  --frame-dark: #1f1f22;  --frame-edge: rgb(255 255 255 / 14%);  --frame-button: #333336;
  • Each pixel line is an SVG hidden from screen readers, with its real text beside it: “1,320 steps”, “Goal 8,000 steps, by tmrw 19:30”, the pet’s name, stage and experience.
  • The bar is a progressbar with its percentage. The hearts are an image labelled “3 of 3 hearts”.
  • The cat is a button (“Pet Bean”): Enter or Space pets it as a tap does. The walk button is a toggle with aria-pressed.
  • A polite live region says when the pet purrs and the XP it earned, and when a walk starts or pauses; the ticking count is not announced.
  • Both buttons show a focus ring. The demo finger is hidden from screen readers and stops on any key or pointer.
  • Under reduced motion the demo does not play, the cat holds still, no heart rises and the bar and button change without transitions.