Byte
A little voxel critter made of real 3D cubes: it waddles, blinks its pixel eyes, and types a dev command when you tap it.
experimental
Try it
How it works
- Every pixel is a real CSS cube (six faces, lit from the top-left), arranged on a grid: ear blocks, a three-row body, arm nubs, and four legs that waddle in pairs.
- Its eyes are body cubes with a blinking pixel pupil and a glint, with pink cheeks under them. It tilts toward the pointer.
- Tap it and it hops and types a line in a speech bubble, in turn: bun dev, git push, ship it! Byte is exported on its own; pass lines to give it your own.
- Reduced motion stops the waddle, bob, hop, blink, and typing; the bubble still shows the line.
Install
Copy it, with its game rules, into a shadcn/ui project with the shadcn CLI, or open it in v0 to remix it. Or copy the source below.
bunx shadcn@latest add https://streamui.org/r/byte-lab.jsonSource
src/components/demos/ByteLab.tsx
"use client";
import { useEffect, useId, useRef, useState, type CSSProperties, type PointerEvent } from "react";
import { cn } from "@streamuilib/react";
export type ByteColor = "lime" | "orange" | "blue" | "pink";
export type ByteProps = {
color?: ByteColor;
/** Width of the critter, in px. */
size?: number;
/** What it types when tapped, in turn. */
lines?: string[];
/** Accessible name. */
label?: string;
};
const DEFAULT_LINES = ["bun dev", "git push", "ship it!", "fix bugs", "hello, world", "npm i coffee", "it works!"];
type Voxel = { x: number; y: number; z: number; part: "body" | "ear" | "arm" | "leg" | "eye" | "cheek" };
/**
* Byte's voxels, on a grid: x across (0 to 7), y down (0 to 4), z in depth
* (0 to 1). Ears on top, a body three rows tall with eyes in its middle row
* and pink cheeks under them, arm nubs off the sides, and four legs underneath.
*/
const VOXELS: Voxel[] = (() => {
const v: Voxel[] = [];
for (const z of [0, 1]) {
v.push({ x: 1, y: 0, z, part: "ear" }, { x: 6, y: 0, z, part: "ear" });
for (let y = 1; y <= 3; y++) {
for (let x = 0; x <= 7; x++) {
const eye = z === 1 && y === 2 && (x === 2 || x === 5);
const cheek = z === 1 && y === 3 && (x === 1 || x === 6);
v.push({ x, y, z, part: eye ? "eye" : cheek ? "cheek" : "body" });
}
}
}
v.push({ x: -1, y: 2, z: 1, part: "arm" }, { x: 8, y: 2, z: 1, part: "arm" });
for (const x of [1, 3, 4, 6]) v.push({ x, y: 4, z: 1, part: "leg" });
return v;
})();
const COLORS: Record<ByteColor, string> = {
lime: "var(--lime)",
orange: "var(--orange)",
blue: "var(--blue)",
pink: "var(--pink)",
};
/** One cube of the critter: six faces, lit from the top-left. */
function Cube({ v, s, fill }: { v: Voxel; s: number; fill: string }) {
const shade = (by: number) => `color-mix(in oklab, ${fill} ${100 - by}%, var(--ink))`;
const faces: [string, string][] = [
[`translateZ(${s / 2}px)`, v.part === "leg" ? shade(18) : fill],
[`rotateY(180deg) translateZ(${s / 2}px)`, shade(35)],
[`rotateY(90deg) translateZ(${s / 2}px)`, shade(28)],
[`rotateY(-90deg) translateZ(${s / 2}px)`, shade(20)],
[`rotateX(90deg) translateZ(${s / 2}px)`, `color-mix(in oklab, ${fill} 82%, white)`],
[`rotateX(-90deg) translateZ(${s / 2}px)`, shade(45)],
];
return (
<span
aria-hidden
className={cn("absolute block", v.part === "leg" && (v.x === 1 || v.x === 4 ? "byte-leg-a" : "byte-leg-b"))}
style={{ width: s, height: s, left: v.x * s, top: v.y * s, transformStyle: "preserve-3d", transform: `translateZ(${(v.z - 0.5) * s}px)` }}
>
{faces.map(([t, bg], i) => (
// Soft edges between cubes, so it reads as one critter rather than a grid.
<span
key={i}
className="absolute inset-0 block border"
style={{ transform: t, background: i === 0 && v.part === "cheek" ? "var(--pink)" : bg, borderColor: "color-mix(in oklab, var(--ink) 45%, transparent)", backfaceVisibility: "hidden" }}
>
{/* An eye is a body cube with a blinking pixel pupil and a glint. */}
{v.part === "eye" && i === 0 && (
<span className="byte-blink absolute inset-[8%] block bg-ink">
<span className="absolute top-[14%] right-[14%] block size-[28%] bg-paper-raised" />
</span>
)}
</span>
))}
</span>
);
}
/**
* Byte: an original voxel critter in CSS 3D (each pixel is a real cube),
* with ear blocks, arm nubs, blinking pixel eyes, pink cheeks, and four legs
* that waddle. It tilts toward the pointer, and a tap
* makes it hop and type a line in a speech bubble. One labelled button.
*/
export function Byte({ color = "lime", size = 180, lines = DEFAULT_LINES, label = "Byte" }: ByteProps) {
const s = Math.round(size / 9); // voxel edge
const fill = COLORS[color];
const [tilt, setTilt] = useState({ x: -16, y: -28 });
const [said, setSaid] = useState<{ n: number; text: string } | null>(null);
const statusId = useId();
const still = useRef(false);
const count = useRef(0);
useEffect(() => {
still.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}, []);
// The bubble fades after a while.
useEffect(() => {
if (!said) return;
const t = setTimeout(() => setSaid(null), 2600);
return () => clearTimeout(t);
}, [said]);
const onMove = (e: PointerEvent<HTMLButtonElement>) => {
if (still.current) return;
const r = e.currentTarget.getBoundingClientRect();
const dx = (e.clientX - r.left) / r.width - 0.5;
const dy = (e.clientY - r.top) / r.height - 0.5;
setTilt({ x: -16 - dy * 16, y: -28 + dx * 46 });
};
return (
<button
type="button"
aria-label={`${label}. Tap and it types something.`}
aria-describedby={statusId}
onClick={() => {
const text = lines[count.current % lines.length];
count.current += 1;
setSaid({ n: count.current, text });
}}
onPointerMove={onMove}
onPointerLeave={() => setTilt({ x: -16, y: -28 })}
className="relative grid cursor-pointer place-items-center rounded-[3rem] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-blue"
style={{ width: size * 1.55, height: size * 1.15, perspective: 1100 }}
>
<style href="byte-critter" precedence="default">{`
@keyframes byte-bob { 0%, 100% { translate: 0 0; } 50% { translate: 0 -3%; } }
@keyframes byte-hop { 0%, 100% { translate: 0 0; } 35% { translate: 0 -22%; } 55% { translate: 0 0; } 70% { translate: 0 -5%; } }
@keyframes byte-waddle { 0%, 100% { translate: 0 0; } 50% { translate: 0 -30%; } }
@keyframes byte-blink { 0%, 90%, 100% { scale: 1 1; } 94% { scale: 1 0.12; } }
@keyframes byte-type { from { width: 0; } }
@keyframes byte-bubble { from { opacity: 0; scale: 0.6; } 70% { scale: 1.06; } to { opacity: 1; scale: 1; } }
.byte-bob { animation: byte-bob 1.6s ease-in-out infinite; }
.byte-hop { animation: byte-hop 520ms cubic-bezier(.3,0,.4,1); }
.byte-leg-a { animation: byte-waddle 0.5s ease-in-out infinite; }
.byte-leg-b { animation: byte-waddle 0.5s ease-in-out -0.25s infinite; }
.byte-blink { animation: byte-blink 3.4s ease-in-out infinite; }
.byte-type { animation: byte-type 0.7s steps(var(--chars)) both; }
.byte-bubble { animation: byte-bubble 260ms cubic-bezier(.2,.9,.3,1.3) both; transform-origin: 30% 100%; }
.byte-box { transition: transform 160ms ease-out; }
@media (prefers-reduced-motion: reduce) {
.byte-bob, .byte-hop, .byte-leg-a, .byte-leg-b, .byte-blink, .byte-type, .byte-bubble { animation: none; }
.byte-box { transition: none; }
}
`}</style>
<span aria-hidden className="absolute bottom-[20%] left-1/2 block h-[8%] w-[50%] -translate-x-1/2 rounded-[50%] bg-ink/18 blur-[3px]" />
{/* What it typed, in a bubble above its head. */}
{said && (
<span
key={said.n}
aria-hidden
className="byte-bubble absolute top-[10%] left-[50%] z-10 border-[3px] border-ink bg-paper-raised px-2.5 py-1 font-mono text-sm font-bold whitespace-nowrap shadow-[3px_3px_0_var(--ink)]"
>
<span className="text-ink-muted">> </span>
<span className="byte-type inline-block overflow-hidden align-bottom" style={{ "--chars": said.text.length } as CSSProperties}>
{said.text}
</span>
<span className="absolute top-full left-4 -mt-[1px] block size-0 border-x-[7px] border-t-[9px] border-x-transparent border-t-ink" />
</span>
)}
<div className="byte-bob" style={{ width: s * 8, height: s * 5 }}>
<div key={said?.n ?? 0} className={cn(said && "byte-hop")} style={{ width: s * 8, height: s * 5 }}>
<div
className="byte-box relative"
style={{ width: s * 8, height: s * 5, transformStyle: "preserve-3d", transform: `rotateX(${tilt.x}deg) rotateY(${tilt.y}deg)` }}
>
{VOXELS.map((v, i) => (
<Cube key={i} v={v} s={s} fill={fill} />
))}
</div>
</div>
</div>
<span id={statusId} aria-live="polite" className="sr-only">
{said ? `Byte typed: ${said.text}` : ""}
</span>
</button>
);
}
const SWATCHES: ByteColor[] = ["lime", "orange", "blue", "pink"];
/**
* The lab: Byte, with a body color picker. `preview` (the Labs index card)
* shows just Byte; the picker lives on the lab's own page.
*/
export function ByteLab({ preview = false }: { preview?: boolean }) {
const [color, setColor] = useState<ByteColor>("lime");
return (
<div className="flex w-full flex-col items-center gap-4 text-ink">
<Byte color={color} />
{!preview && (
<div role="group" aria-label="Color" className="flex gap-2">
{SWATCHES.map((c) => (
<button
key={c}
type="button"
aria-label={c}
aria-pressed={color === c}
onClick={() => setColor(c)}
className={cn("size-8 border-2 border-ink focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-blue", color === c && "ring-2 ring-ink ring-offset-2")}
style={{ background: COLORS[c] }}
/>
))}
</div>
)}
</div>
);
}