Save slots
NewA game’s “Select save data to load.” screen: slot plates with a painted thumbnail, date, place and Autosave tag, a bright sliding frame and a load question.
save-slots/save-slots.tsxSelect save data to load.
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 load screen between a game’s title menu and play, where the player picks which save to resume.
- Saves that are told apart by a moment, not a name: a picture of where they were, the place and the date.
- A mix of manual saves and saves the game made on its own, which the player should tell apart at a glance (the Autosave tag).
- A screen driven by a controller or the keyboard first, with a cursor that moves one slot at a time and an A / B pair to choose and back out.
- Loading something that throws away the current play, so it is worth one “Load this save data?” before it happens.
When not to use
- Picking a file or document in an app. Use a file list or an open dialog with names, sizes and dates.
- A single continue button with one save behind it. Use a title menu item such as Continue.
- Choosing between a few short options with no picture or date. Use a choice list or a pause menu.
- Saving over a slot, where the question is about losing data. Give it its own overwrite flow with a stronger warning, not this load question.
- A page that should follow the reader’s light or dark theme: this is a dark game screen on any page. Use a plain list or card grid.
You can add this component with the shadcn CLI or by hand:
Into components/save-slots of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/save-slots.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 { SaveSlots, type SaveSlot } from './save-slots';const saves: SaveSlot[] = [ { id: 'a', savedAt: '05/10/2026 16:05', location: 'Amber Highlands', autosave: true, scene: 'dunes' }, { id: 'b', savedAt: '24/09/2026 13:25', location: 'Stoneback Pass', scene: 'arch' },];export function LoadScreen({ onBack }: { onBack: () => void }) { return ( <SaveSlots slots={saves} autoplay={false} onLoad={(slot) => console.log('load', slot.id)} onCancel={onBack} /> );}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 screen between the title menu and play, where the player picks the save to resume. Each slot is a dark plate with a small painted picture of the moment, the date and time, the place in large italics and a grey Autosave tag when the game saved by itself. A bright cream frame slides to the slot under the cursor, and that slot’s picture fills the back of the screen, blurred. Choosing asks “Load this save data?” with two choice pills; loading fades to black with a spinner. Cancel (B) waits in the corner. Browse with the arrows, the wheel, a hover or a gamepad.
- The screen is one drawing in design units: --u comes from the stage’s width by a container query unit (min(1px, 100cqi / 960)), and every size is a multiple of it, so the 960 × 540 screen scales as a whole and keeps the kit’s proportions: 360 × 62 slot plates on a 70-unit pitch, a 130-unit thumbnail, 10-unit date and 14-unit place. On a stage under 600 px the screen becomes a 392 × 600 portrait one, with the same plates.
- The thumbnails are original painted landscapes, one SVG per scene (dunes, a rock arch, docks, a cliff, a basin, a snowfield, a night wood), layered shapes and gradients with a small traveller in each, cropped to cover the plate as a screenshot would. The selected one is drawn again behind everything, enlarged, blurred and half transparent under a night-blue shade; moving the cursor crossfades it to the next one.
- The frame is a single element in the column, moved by translate to the selected row with an overshooting curve (190 ms), so it slides rather than jumps. It is a cream line inside a dark olive one, with a faint grey halo, a soft glow that breathes, and four small cream corner wedges drawn as corner gradients. At either end of the list the frame only nudges.
- The column scrolls by translate too, only when the cursor nears its edges, so a row of context stays visible below it; its foot fades out by a mask, as the kit’s last slot does, and its top fades once it has scrolled.
- Choosing flashes the framed plate and opens the question: the screen dims, “Load this save data?” and the slot’s place and date rise in, with Load and Cancel (B) choice pills, the chosen one framed in cream with a pointer. Loading fades the screen to black over 420 ms with a dot spinner while the backdrop drifts forward, then the list comes back and its plates rise in again one by one, 45 ms apart.
- Input: the arrows or W and S move, A, Enter or Space choose, B, Escape or Backspace go back, while focus is inside; the wheel steps the cursor over the list (the page scrolls on past either end); a mouse hover moves the cursor and a click loads; on touch, a first tap selects and a second chooses; a connected gamepad’s d-pad, stick, A and B work too.
- Uncontrolled, a demo browses down and back, opens the question, backs out with B, and loads a save, while the screen is on view and untouched; any real input hands it to the reader. Given selected and onSelect, the cursor is the game’s.
| Prop | Type | Default | Description |
|---|---|---|---|
slots | SaveSlot[] | seven sample saves | The saves: { id, location, savedAt, autosave?, scene? }. savedAt is shown as given; scene picks the painted thumbnail (dunes, arch, docks, cliff, basin, snow, forest). |
selected | number | — | The slot under the cursor, controlled, as an index into slots. Turns the demo off. |
defaultSelected | number | 0 | Uncontrolled, the slot the cursor starts on. |
defaultPhase | 'list' | 'confirm' | 'loading' | 'list' | Opens on the list, on the load question, or on the loading screen, which then holds as a still. Set, it turns the demo off. |
onSelect | (index: number) => void | — | Called when the cursor moves to another slot. |
onLoad | (slot: SaveSlot, index: number) => void | — | Called when a save is loaded, after the question when confirm is on. |
onCancel | () => void | — | Called on Cancel (B), from the list. |
prompt | string | 'Select save data to load.' | The line above the list. |
confirm | boolean | true | Ask “Load this save data?” before loading. |
keys | boolean | true | Listen to the keyboard while focus is inside, and to a connected gamepad. |
autoplay | boolean | true | While on screen and untouched, a short demo: browsing, asking, backing out and loading. Any input stops it. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark, or the reader’s; without it, the page’s color-scheme. The screen keeps its own dark colours; its edge, shadow and focus ring follow the scheme. Set --slots-unit on the root to fix the size (1px is the 960 × 540 screen). |
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 { useEffect, useEffectEvent, useId, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent,} from 'react';import { SceneArt, type Scene } from './scenes';import { useAutoplay } from './use-autoplay';import styles from './save-slots.module.css';/* * Save slots: the screen between a game's title menu and play, where the player picks the save to * resume. "Select save data to load." over a column of slots, each a dark plate with a picture of * the moment (a small painted landscape, scenes.tsx), the date and time, the place, and an * Autosave tag when the game saved by itself. A bright frame marks the slot under the cursor, and * that slot's picture, blown up and blurred, fills the back of the screen. Cancel (B) sits in the * corner./* Save slots (save-slots.tsx): a whole game screen, so it draws its own dark surface: the selected slot's picture blurred behind, the prompt, a column of slot plates with a bright frame on one, Cancel (B) in the corner, and the load question and loading screen over it. Every size is a multiple of --u, one design unit (1 px at full size) of a 960 × 540 screen (--sw × --sh), or 392 × 600 on a narrow stage. --u comes from the stage's width by a container query unit, so the screen scales as one drawing; --slots-unit fixes it. Slots are 360 × 62 on a 70-unit pitch, as the kit draws them at this scale. Colours are the tokens below. The screen keeps its colours on any page, as a game screen does; only the focus ring, the screen's edge and its shadow follow the scheme, through light-dark(). Motion: the frame slides by translate with an overshoot; the column scrolls by translate; the backdrop crossfades; the rows rise in by keyframes, staggered by --rise; the question and the loading screen fade and scale in. All of it stops under reduced motion, and the frame's breath pauses off screen. */.root { /* The screen. */ --night: #030b14; --ink: #ebe7db; --ink-dim: #d9d5c8; --plate: #020305; --plate-on: #121210;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The slots' pictures: small painted landscapes drawn in SVG, one per scene, in place of the * screenshots a game would take when it saves. Each is a 260 × 120 drawing of layered shapes and * gradients (sky, far ground, near ground, light) with a tiny traveller in it, so a thumbnail reads * as "a moment of play" without any real game's art. The same drawing, scaled up and blurred by * the CSS, is the screen's backdrop for the selected slot. */import type { CSSProperties } from 'react';export type Scene = 'dunes' | 'arch' | 'docks' | 'cliff' | 'basin' | 'snow' | 'forest';export const SCENES: Scene[] = ['dunes', 'arch', 'docks', 'cliff', 'basin', 'snow', 'forest'];/** A small traveller seen from behind: hooded head, cloak, a pack and a staff. */function Traveller({ x, y, s = 1, cloak, skin = '#3a2a1e', lean = 0,}: {// 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 slots are a listbox named by the prompt, with one option per slot (“Amber Highlands, 05/10/2026 16:05, autosave”); the list keeps focus and aria-activedescendant follows the cursor, so screen readers read each slot as it is reached.
- The question is an alertdialog named “Load this save data?”; focus moves to its Load pill when it opens and back to the list when it closes. Cancel (B) is a real button.
- A polite live region says when the question opens, when a save is loading and on cancel.
- Focus shows as a white outline round the frame and a ring round the screen, and as an outline on the pills and the Cancel button.
- Under reduced motion the demo does not play and every change is instant: no sliding frame, rising rows, breathing glow or drifting backdrop. The pictures are decorative and hidden from assistive technology.