Creature compendium
NewA game tablet’s field guide: numbered frames that fill with pictures as things are photographed, category tabs, page arrows, a gliding cursor and a detail page the picture flies into.
creature-compendium/creature-compendium.tsxField Compendium
012 Cliff Goat
Hearts Recovered
Description
A sure-footed goat that lives on ledges no predator can reach. Its thick fleece keeps it warm through the coldest nights on the heights.
Common Locations
- Greyhorn Slopes
- Lantern Hills
Cooking Effect
Cold Resist
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 collection the player fills over the game: creatures, enemies, materials or gear that turn from unknown to sighted to registered as they are found or photographed.
- A numbered catalogue where the gaps matter: an empty frame with its number tells the player how much is left to find, and where.
- A grid browsed with a controller: one cursor moved by the d-pad or stick, shoulders for categories, triggers for pages, A to open and B to go back.
- Entries with a picture and a little lore each (where it lives, what it gives, a stat), read on a detail page the picture flies into.
- A tablet or device screen inside the game, where a stylised in-world frame is part of the fiction.
When not to use
- An inventory the player uses or equips from. Use an item grid with counts and actions; the compendium only records what was found.
- A short list of quests or objectives. Use the quest log, whose rows carry titles, places and a story.
- An achievement or trophy list with progress bars. Use an achievement list; this grid shows pictures, not progress.
- A photo gallery of the player’s own shots. Use the photo album, which keeps every picture rather than one per entry.
- A product catalogue or image gallery in an app. Use a card grid with filters and plain focus; the tablet’s bezel, script and controller hints belong to a game.
You can add this component with the shadcn CLI or by hand:
Into components/creature-compendium of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/creature-compendium.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
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
import { CreatureCompendium, DEFAULT_ENTRIES } from './creature-compendium';export function Guide({ seen }: { seen: Set<string> }) { const entries = DEFAULT_ENTRIES.map((entry) => seen.has(entry.id) ? { ...entry, status: 'registered' as const } : entry, ); return ( <CreatureCompendium entries={entries} defaultSelected="cliff-goat" onTrack={(entry) => console.log('tracking', entry?.name ?? 'nothing')} /> );}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.
The guide a player fills by photographing what they meet, on a navy tablet screen in a carved stone bezel. Numbered frames, 001 to 096 across five categories, show a picture once an entry is registered, a silhouette once it has been sighted, and only a lens mark and the name in the tablet’s script before that. Cyan corner marks glide from frame to frame and name the one they rest on; tabs jump between creatures, monsters, materials, equipment and treasures, and arrows turn pages. Open an entry and its picture flies out of its frame into a large one, beside a description, where it is commonly found and what it gives; step through the guide, track an entry with the sensor, and fly back. Arrow keys, the controller’s letters, a gamepad or the pointer.
- Every size is a multiple of one design unit, --u, worked out on the tablet from the component’s width with a container query unit, so the screen scales as one drawing: a 1016 × 572 screen with a 45-unit header and a 44-unit footer, 93-unit frames on a 107 × 115 pitch in eight columns, 25-unit tab icons on a 45.75-unit pitch, a 223-unit picture on the detail page. Under 720 px the screen is 456 units wide with four columns, still 24 entries a page, and the detail page stacks.
- Entries are data: a name, a category, a status (registered, sighted or unknown), a picture (shape, palette, habitat) and, once registered, a description, places, a stat and an effect. They are numbered by their place in the whole guide; each category starts a new page of 24, and the dots over a tab count its pages, the current one lit.
- The cursor is one element over the grid, placed by a transform from its column and row: a step glides in 160 ms with a small overshoot; against an end it bumps 6 units and springs back (the Web Animations API on the separate translate property). Its four cyan corner marks breathe outwards. Off the last column the next page slides in from the right, column by column, 16 ms apart, and the cursor drops in; turning back slides the other way.
- Opening an entry measures its frame and the big frame and plays the big one from the frame’s place and size to its own (FLIP, 380 ms); the grid fades out, the section rules draw outwards from their titles and the text rises in a short stagger. Closing fades the text at once and flies the picture back into the frame it came from while the grid returns. Left and right step through registered entries, sliding in from the side pressed.
- A frame is CSS: a light outer line, a dark gap, an inner line, an inner cyan glow and four chamfered corners drawn by gradients; the selected frame turns white and its name replaces the script line. When an entry turns registered, its frame flashes white like a shutter and the picture develops from white to colour with a pop.
- All art is SVG drawn for this component: each picture is a setting (meadow, forest, coast, snow, desert, cave, ruins) and a flat subject with a thin dark outline; a sighted entry draws the same subject with every fill forced to one dark cyan by CSS. The tablet’s script writes each letter as a stem with hooks, bars and dots; the bezel is a stepped stone bracket with a lens boss in each corner and a carved band of blocks and studs along the bars, glowing blue.
- Uncontrolled, a demo plays while it is on screen and untouched: browse, open an entry, step twice, track it, close, change category, turn a page, register a new butterfly and open it. Any real input hands it to the reader where the demo left it.
| Prop | Type | Default | Description |
|---|---|---|---|
entries | CompendiumEntry[] | a sample guide of 96 | The guide, in order: { id, name, category, status, shape, palette, habitat, description?, locations?, stat?, effect? }. Numbered in category order; an entry whose status turns registered flashes and develops. Turns the demo off. |
title | string | 'Field Compendium' | The name on the tablet’s header. |
defaultSelected | string | — | The id of the entry the cursor starts on. Turns the demo off. |
onSelect | (entry: CompendiumEntry) => void | — | Called when the cursor lands on an entry. |
defaultOpen | boolean | false | Start on the selected entry’s detail page. Turns the demo off. |
onOpenChange | (open: boolean, entry: CompendiumEntry) => void | — | Called when the detail page opens or closes. |
tracked | string | null | — | The id of the entry the sensor tracks, controlled; pair it with onTrack. |
defaultTracked | string | null | null | Uncontrolled, the entry tracked at first. |
onTrack | (entry: CompendiumEntry | null) => void | — | Called when the reader tracks a registered entry, or stops tracking (null). |
keys | boolean | true | Listen to the keyboard while the component has focus, and to a connected gamepad. |
autoplay | boolean | true | While on screen and untouched, a short browse: moves, open, step, track, a category, a page and a new entry registered. Any input stops it. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark, or the reader’s; without it, the page’s color-scheme. The tablet keeps its colours on both; its edge, shadow and the focus ring follow it. |
className | string | — | Added to the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useEffectEvent, useId, useLayoutEffect, useRef, useState, type CSSProperties,} from 'react';import { BezelBand, BezelCorner, EffectGlyph, EntryArt, FaceGlyph, Heart, KeyGlyph, Lens, PageArrow,/* Creature compendium (creature-compendium.tsx): a tablet screen showing a numbered field guide, centred on a plain stage. Every size is a multiple of --u, one design unit (1 px at full size), set on the tablet from the component's width (a container query unit), so the screen scales as one drawing and keeps its proportions: a 1016 × 572 screen with a 45-unit header and a 44-unit footer, 93-unit frames on a 107 × 115 pitch in eight columns, 25-unit tab icons on a 45.75 pitch, a 223-unit picture on the detail page. Under 720 px the screen is 456 units wide, four columns and taller, and the detail page stacks. Colours are the tokens below. The screen keeps its colours in both schemes, as a game's tablet does: navy glass, cyan type, white selection, a stone bezel with a blue glow. Only the screen's outer edge and shadow and the focus ring follow the scheme, through light-dark(). Motion: the cursor glides by a transform transition; bumps, the picture's flight between its frame and the big frame, and nothing else, are played by the script (Web Animations API); page turns, tab pops, rules drawing, text rising, the shutter flash and the sensor's ping are CSS animations, replayed by remounting their element. */.root { /* The tablet's own colours. */ --bar: #0d0d0f; --navy: #171a38; --navy-lit: #1e2249;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The compendium's data: five categories and the 96 entries of the sample guide. An entry is * numbered by its place in the whole list (001…096); a category starts on a new page, and a page * holds 24 entries. Only registered entries carry a description, places and a stat; a sighted one * shows as a silhouette, an unknown one as an empty frame with its name in the tablet's script. */export type Category = 'creatures' | 'monsters' | 'materials' | 'equipment' | 'treasures';export type Status = 'registered' | 'sighted' | 'unknown';/** The picture drawn for an entry (see art.tsx). */export type Shape = | 'fox' | 'deer' | 'boar' | 'goat' | 'hare' | 'songbird' | 'heron' | 'fish' | 'crab'// Copyright (c) 2026 Bruno Delorence. MIT License.import type { CSSProperties, ReactNode, SVGProps } from 'react';import type { Category, EffectIcon, Habitat, Palette, Shape } from './entries';/* * All the compendium's art, drawn for it as SVG: the entry pictures (a setting and a subject in a * 100-unit box, flat colours with a thin dark outline, so they read as small "photos"), the tab and * effect icons, the empty-frame lens, the tablet's script, the controller glyphs and the bezel. * A sighted entry draws the same subject with every fill and stroke forced to one colour by CSS * (the `silhouette` class), so no second set of drawings is needed. */const INK = '#1d1a22';const line = { stroke: INK, strokeWidth: 1.4, strokeLinejoin: 'round', strokeLinecap: 'round',} as const;// ---- Settings ----function Scene({ habitat, id }: { habitat: Habitat; id: string }) {// 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` alone says whether it is on screen * in a visible tab, for clocks that should stop when nobody can see them. */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);- The categories are a real tab list (arrow keys move along it, the open category is the tab in the tab order), and the page is a listbox whose options are the entries, with the cursor’s entry as the active descendant, so a screen reader reads each one as the cursor reaches it: “012, Cliff Goat”, “011, sighted, not registered”.
- Opening an entry moves focus to its detail page, a labelled group with real headings for its name and sections; closing returns focus to the grid. The hidden view is inert, so neither keyboard nor screen reader can reach it.
- The page and entry arrows, Open, Back and Track are real buttons with their controller keys in aria-keyshortcuts; the sensor is a status that names what it tracks. A polite live region reports page and category changes, tracking and a newly registered entry.
- Focus shows as an outline round the tab, the grid or the button that has it, blue on a light page and light blue on a dark one; the cursor’s corner marks show the entry inside the grid.
- All pictures, the script and the bezel are hidden from assistive technology. Under reduced motion the demo does not play and every change lands at once: no glide, bump, flight, page slide, shutter or pulse.