Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

System menu

New

A game pause screen’s system page: a list of session verbs with a bright, breathing selection frame, confirm dialogs, and a handheld diagram with cyan callouts.

system-menu/system-menu.tsx
Live · interactive

Basic Controls

  • Focus: ZL button
  • Move: left stick[Press] Crouch
  • Jump: X buttonAction: A buttonPut Away: B button
  • Camera Controls: right stick

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

  • The System page of a game’s pause menu: a short, fixed list of verbs about the session (save, load, options, quit to title) moved through with a pad or the keys.
  • A menu where one item is always selected and the selection itself is the cursor: hovering or the stick moves it, A or Enter runs it.
  • Verbs that change the session and must ask first, with the safe answer chosen by default on the destructive ones (Load, Return to Title).
  • Showing the controls next to the menu, as a console diagram with callouts paged by scheme (basic, combat, travel).
  • Items that unlock over the game: a locked one stays in its place as “? ? ?” and refuses with a shake.

When not to use

  • An app or website’s settings. Use a settings page with labelled form controls; this is a game screen on its own dark plate.
  • A long or searchable list of choices. Use a list with a filter or a command palette.
  • A menu opened from a button in a toolbar. Use a dropdown menu with role="menu".
  • Choosing between save files. Use save slots, which show each file’s place, time and progress.
  • Teaching one input at the moment it is needed. Use a controller prompt next to the action, not a whole controls page.

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

Into components/system-menu of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/system-menu.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

pause-system.tsx
import { SystemMenu } from './system-menu';export function PauseSystem({ save, load, quit }: { save(): void; load(): void; quit(): void }) {  return (    <SystemMenu      autoplay={false}      onConfirm={(id) => {        if (id === 'save') save();        if (id === 'load') load();        if (id === 'return-to-title') quit();      }}    />  );}

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 page a player opens to stop the world and manage the session. A short list of verbs in two groups (Save, Load, then Options, Ability Controls, Extras, Return to Title) where exactly one item wears the cream frame with corner ticks and a soft glow, so the selection reads from across a room. Verbs that change the session ask first, in a small dialog with its own framed Yes and No, then run a bar and a check. Beside the list, a generic handheld console carries callouts for what each button does, paged like a game’s controls pages: the plates wipe out of their cyan bars, the leaders draw to their buttons and a ring pops round each target. It all sits on its own dark plate, so it reads on a light or a dark page.

  • Every size on the plate is a multiple of one design unit, --u, worked out from the component’s width with a container query unit (min(1px, 100cqi / 900)), so the screen scales as one drawing and keeps the kit’s proportions: 175 × 38 items on a 50-unit pitch with a 38-unit gap between groups, a 624 × 280 diagram, 158-unit callout plates and 14.75-unit italic type. Under 720 px the list stacks above the diagram and keeps real pixels; under 488 px the diagram keeps the console alone with numbered markers and lists the callouts underneath.
  • The selected frame is one span over the item: a 1.5-unit cream border, four corner ticks drawn as small linear-gradient triangles, and an outer and inner glow as box-shadows. It is mounted only on the selected item, so moving the selection blooms a new frame in (scale 1.06 to 0.99 to 1 in 220 ms) and drops the old one; once it rests, its glow breathes on a 2.4-second alternate loop. A confirm remounts it with a dip and a cream flash; a locked item shakes its label instead.
  • The controls diagram is laid out from data. Each callout names a target on the console (a stick, the face buttons, the d-pad, a shoulder key); the plates of each side are stacked at their target’s height and pushed down so they never overlap, and each leader runs from the plate’s cyan bar straight to the target’s edge when the target is level with the plate, or across and then up or down to its centre, ending in a dot.
  • Paging remounts the diagram: plates open by clip-path from their bar outwards, staggered by 45 ms; leaders draw by stroke-dashoffset on a pathLength of 1; a cyan ring (a circle, a rounded diamond round the face buttons, or a pill round a shoulder key) pops in with an overshoot; the title slides in from the paging side. Hovering a plate thickens its leader and lights its ring.
  • The console is a generic handheld drawn for the component in SVG: one body with two grips, a screen in a grey bezel, a d-pad over a stick on the left, four face buttons over a stick on the right and small minus and plus keys, with the same console seen from its top edge underneath, where the shoulder keys and triggers are. The callout glyphs share the lab’s game glyph style: light discs, keys and stick caps with dark letters.
  • Confirming a verb with a question opens a dialog over the plate (a scrim, then a plate that scales up from 0.96) with Yes and No as framed items; destructive verbs start on No. Yes runs a 1.3-second bar, then a check pops with the done line, and the dialog closes itself. Uncontrolled, a demo walks the list, pages the controls twice, climbs back and saves, while the menu is on screen and untouched.
PropTypeDefaultDescription
itemsSystemMenuItem[]—The verbs, top to bottom: { id, label, confirm?, working?, done?, danger?, locked?, group? }. A confirm question makes the verb ask first; danger starts the answer on No; locked shows "? ? ?" and refuses; a change of group leaves a gap.
pagesControlsPage[]—The controls diagram’s pages: { title, callouts }, each callout { side, target, lines: [{ glyph?, label, sub? }] } pointing at a console target (ls, rs, dpad, face, l, zl, r, zr, minus, plus).
selectednumber—The selected item’s index, controlled. Turns the demo off.
defaultSelectednumber0Uncontrolled, the item selected at first.
onSelectedChange(index: number) => void—Called with the new index when the selection moves.
pagenumber—The controls page shown, controlled. Turns the demo off.
defaultPagenumber0Uncontrolled, the controls page shown at first.
onPageChange(page: number) => void—Called with the new page index when the controls are paged.
onConfirm(id: string) => void—Called with an item’s id when it runs: at once, or after its dialog’s Yes.
defaultDialogstring—An item’s id: opens with its confirm dialog asking, its default answer chosen, and that item selected. Turns the demo off.
autoplaybooleantruePlays the demo while the menu is on screen and untouched; any real input hands it to the reader.
colorScheme'light' | 'dark' | 'system'—Forces a colour scheme for the plate’s edge and shadow; inherited from the page when unset.
classNamestring—A class for the root.

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

system-menu.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useCallback,  useEffect,  useEffectEvent,  useId,  useRef,  useState,  type CSSProperties,  type KeyboardEvent,} from 'react';import {  Console,  Flourish,  Glyph,  glyphName,  TARGETS,  type ConsoleTarget,  type ControlGlyph,} from './glyphs';import { useAutoplay } from './use-autoplay';
system-menu.module.css
/* System menu (system-menu.tsx): the pause screen's system list beside a controls diagram, on a   dark plate of its own.   Every size inside the plate is a multiple of --u, one design unit (1 px at full size), set from   the component's width with a container query unit, so the screen scales as one drawing and keeps   the kit's proportions: 175 × 38 items on a 50-unit pitch, a 624 × 280 diagram, 158-unit callout   plates. Under 720 px the list stacks above the diagram (the list then keeps real pixels); under   488 px the diagram keeps the console alone with numbered markers and lists the callouts below.   Colours are the tokens below. The game's pieces keep their colours, as a game's do: the plate,   the near-black items, the cream frame and type, the cyan callouts, the light console. Only what   touches the page follows the scheme, through light-dark(): the plate's edge and shadow.   Motion: the frame blooms in on selection (scale and glow) and breathes while it rests; a press   remounts it with a dip and a flash; a refusal shakes the label. Paging wipes the plates out of   their bars by clip-path, draws the leaders by stroke-dashoffset and pops the targets' rings. */.root {  /* The game screen. */  --plate-top: #1d1e1a;  --plate-bottom: #10110f;  --rule: rgb(236 232 218 / 0.16);  --ink: #ebe7d9;  --ink-dim: rgb(235 231 217 / 0.4);
glyphs.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The button glyphs of the callouts, and the console they point at, as small SVGs. Glyphs use one * light fill (--glyph) and one dark ink (--glyph-ink), like the other game pieces of the lab: * * - Face buttons (A, B, X, Y, +, −): a disc with the letter upright and bold. * - Shoulders (L, R) and triggers (ZL, ZR): a key with its outer top corner cut, or sloping. * - D-pad: a cross. Sticks: a cap seen slightly from above, with its letter. * * The console is a generic handheld drawn for this component: one body with two rounded grips, a * screen in a grey bezel, a d-pad over a stick on the left, four face buttons over a stick on the * right, small − and + keys, and the shoulder keys peeking over its top edge. Under it, the same * console seen from its top edge, where the shoulder keys and triggers are. Its local box is * 300 × 260 (front view 0–150, top edge 210–256); TARGETS gives the centre and reach of every * control, for the callouts' leaders. */export type ControlGlyph =  'A' | 'B' | 'X' | 'Y' | '+' | '-' | 'L' | 'R' | 'ZL' | 'ZR' | 'dpad' | 'ls' | 'rs';/** The parts of the console a callout can point at. */export type ConsoleTarget =  'ls' | 'rs' | 'dpad' | 'face' | 'l' | 'zl' | 'r' | 'zr' | 'minus' | 'plus';
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. */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(
  • The list is a navigation landmark of real buttons with one tab stop (a roving tabindex): the arrow keys or W and S move the selection and the focus together, Home and End jump to the ends, and Enter, Space or A confirms. The selected item carries aria-current; verbs that ask first carry aria-haspopup="dialog"; a locked item is aria-disabled and named "Locked".
  • The confirm dialog is a modal dialog labelled by its question. Focus moves to its default answer when the reader opens it, the arrow keys switch answers, Escape or B answers No, and focus returns to the item when it closes. Its working and done lines are announced through a polite live region, as are selection moves and page changes made by the reader (never the demo’s).
  • The controls are a labelled section; Q and E or the left and right arrows page it, and the flourishes beside the title are buttons named "Previous controls page" and "Next controls page". Each callout is a list item with its verb and the button’s name in text; the console drawing, leaders and rings are aria-hidden.
  • Focus shows as a blue outline outside the frame. Under reduced motion, the frame, plates, rings, leaders and dialog only fade, the label never shakes and nothing breathes; the demo does not play.