Game over screen
NewA game’s game over screen: a glowing red serif title that fades in slowly, then Continue and Quit under a selection frame, and a hint line.
game-over-screen/game-over-screen.tsxContinue from last save.
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 moment the player falls and the run is over: the world is gone and the game needs one decision before anything else.
- Two or three ways back in, such as Continue from the last save, Retry from a checkpoint or Quit to the title, each with a line saying what it does.
- A full-screen beat that should feel final but short: a slow arrival, then a choice the player can make with a pad, the keys or a click.
- Screens where a button still being mashed from the fight must not skip the choice: input counts only once the options are in.
When not to use
- A pause during play, where the world should stay visible behind the menu. Use a system menu or a pause overlay.
- A win, a level clear or a score summary with stats to read. Use a results screen.
- A question about one action, such as “Load this save data?”. Use a game modal with two choices.
- Losing a life that the player keeps playing through. Use the HUD (a hearts row or a heart meter) and a short toast.
- An error or a confirmation in an app outside a game. Use a dialog or an alert.
You can add this component with the shadcn CLI or by hand:
Into components/game-over-screen of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/game-over-screen.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 { GameOverScreen } from './game-over-screen';export function Defeat({ dead, onRetry, onQuit }: { dead: boolean; onRetry: () => void; onQuit: () => void }) { return ( <GameOverScreen open={dead} options={[ { label: 'Continue', hint: 'Continue from last save.' }, { label: 'Quit', hint: 'Return to the title screen.' }, ]} onChoose={(index) => (index === 0 ? onRetry() : onQuit())} /> );}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.
What a game shows after the player falls. The screen goes black and quiet; the title fades in while its red glow builds, and only once it has landed do Continue and Quit rise in, the selected one wearing a light frame with a soft glow and a small triangle in each inside corner. A hint at the bottom says what the selected option does, and a small star in the corner breathes so the screen reads as waiting. Move with the arrows or W and S, choose with Enter or A: the frame flashes and the screen fades back to black.
- The screen is designed at 1012 × 569 units. One design unit, --u, follows the component’s width through a container query unit (100cqi / 1012, kept between 0.82 px and 1.25 px, or --gos-unit when set). Below about 830 px wide the unit stops shrinking and the screen grows taller than 16:9 instead, so the type stays readable; everything is placed from the centre or the bottom edge, so the layout holds at any height.
- The title is real text in a system serif, bold: the first letter of each word at full size and the rest at 0.75 em, so it reads as small capitals, condensed to 0.755 of its width (a scale on the text, so it stays vector-sharp) to match narrow display capitals. Its glow is three red text shadows (4, 12 and 22 units) around #ff2800.
- The options are buttons 202 × 37 units, 50 units apart: black with a faint edge, or a dark fill when selected. The selection frame is one element over them, a 1.2-unit light line with an outer glow, a warm inner glow and four corner triangles drawn as gradients. Each move remounts it on the new option, so it lands slightly large and tightens in 260 ms, as a game cursor snaps; it then breathes, its glow swelling every 2.4 s.
- The arrival is keyframes that run while data-phase is arrive or ready, so they start once and do not restart when choices open: a 350 ms beat of black, the title fading in from 1.06 × over 1.8 s while its glow overshoots and settles, the options rising 8 units at 1.9 s and 2.04 s, the frame at 2.15 s, the hint at 2.45 s and the star at 2.7 s. Choices count from 2.6 s, so a button mashed from the fight does not skip the screen.
- Choosing flashes the frame white and dips the button to 0.97 for 420 ms, then the whole screen fades out over 900 ms and reports the choice. Uncontrolled, a beat of black follows and the arrival plays again; with open, the screen waits until open goes false and back to true.
- Uncontrolled, a demo plays while the screen is on screen and untouched: the arrival, the selection down to Quit (the hint changes to its line) and back, Continue chosen, the fade, and again. Any key, tap or click hands it to the reader.
- It is a whole screen, so it draws its own black surface with no picture behind it, and keeps its colours on a light page and a dark one; on a dark page a faint light edge (a light-dark() token) keeps it apart from the page.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | 'Game Over' | The title, set in the glowing red serif small capitals. |
options | GameOverOption[] | Continue, Quit | The options, top to bottom, each { label, hint? }: the hint is the line shown at the bottom while that option is selected. |
selected | number | — | The selected option’s index, controlled. Turns the demo off. |
defaultSelected | number | 0 | Uncontrolled, the option selected each time the screen arrives. |
onSelectedChange | (index: number) => void | — | Called when the selection moves, from keys, hover or focus. |
onChoose | (index: number, option: GameOverOption) => void | — | Called when an option is chosen (click, Enter, Space or A), as the frame flashes and the screen starts to fade out. |
open | boolean | — | Controlled arrival: true plays it, false fades the screen to black; after a choice the screen stays dark until open goes false and true again. Without it, the arrival plays when the screen comes into view and again after each choice. Turns the demo off. |
glyph | boolean | true | Show the breathing star in the bottom-left corner. |
autoplay | boolean | true | While on screen and untouched, a loop of the arrival, a move down and back, and a choice. Any input stops it. |
still | 'ready' | 'chosen' | 'dark' | — | Holds the screen in one moment with no arrival, demo or fade: ready (the options in), chosen (the selected option just chosen) or dark (the black before arrival). For stills and documentation; turns the demo off. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark, or the reader’s; without it, the page’s color-scheme. The screen keeps its colours on both; the edge and the page focus colour follow it. Set --gos-unit on the root to fix the size (1px is the 1012 × 569 design). |
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, useRef, useState, type CSSProperties, type KeyboardEvent,} from 'react';import { useAutoplay } from './use-autoplay';import styles from './game-over-screen.module.css';/* * Game over screen: what a game shows after the player falls. The world is gone, the screen is * black and quiet, and it has one job: say it is over, then get the player back in quickly. * * - The title is the only colour on the screen: a red serif in small capitals (the first letter of * each word full height, the rest at 0.75 em), condensed to 0.755 of its width the way display * capitals are (still vector text), glowing with layered red text shadows./* Game over screen (game-over-screen.tsx): a whole screen, so it draws its own black surface. Every size is a multiple of --u, one design unit: the screen is designed at 1012 × 569 units, and --u follows the component's width (a container query unit) between 0.82 px and 1.25 px, or --gos-unit when set. Below about 830 px wide the unit stops shrinking and the screen grows taller than 16:9 instead; everything is placed from the centre or the bottom, so it holds. Colours are the tokens below. The screen keeps its own: black, a red title, light ink. What sits on the page itself follows the scheme through light-dark(): a faint edge that keeps the black screen apart from a dark page, and the focus outline. Motion (all transform, opacity and shadows): arrival keyframes run while data-phase is arrive or ready (so they start when it turns arrive, and do not restart when it turns ready); the title fades in as its glow builds, then the options rise one by one, then the hint and the star. The frame jumps to the selected option and lands with a short settle; a choice flashes it, and leaving fades the whole stage out. */.root { --screen: #000; --red: #ff2800; --red-glow: rgb(255 40 0 / 0.7); --red-haze: rgb(255 40 0 / 0.38); --ink: #e2ded3; --option: #000;// 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 screen is a section named by its title, a real h2 (“Game Over”); the condensed small-capital drawing of it is hidden from assistive technology so the name reads as words.
- The options are real buttons in a group, with a roving tab stop: Tab reaches the selected one, the arrows or W and S move the selection and focus (wrapping), and Enter, Space or A choose. A mouse selects by hovering.
- The selected button carries aria-current and is described by the hint line, which is a polite live region, so moving the selection reads what the option does (“Return to the title screen.”).
- Until the options have arrived the buttons are aria-disabled and choices are ignored. After a choice, when the screen arrives again, focus returns to the first option if it was inside the screen.
- Focus shows as a light outline around the focused button, outside the selection frame. The corner star is decorative and hidden.
- Under reduced motion the demo does not play and nothing animates: the screen appears whole, moves are instant, and a choice cuts to black and back.