Title screen
NewA game’s first screen on a navy tablet bezel: a painted sunset cliff with a lone traveller, a serif wordmark, “Press any button”, and a right-aligned menu with a sliding pointer.
title-screen/title-screen.tsxVer. 1.4.2Data Ver. 3.0© 2026 Eventide
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 first screen of a game or a game-like experience: the name, a call to start, then the few ways in (continue, new game, options, extras).
- A boot or attract screen that waits on its own and starts on any key, click, tap or gamepad button.
- A short main menu read with a controller: up and down move one pointer, A chooses, B goes back to the intro.
- A launch screen that should set the mood with a painted scene and a wordmark before any choice is asked of the player.
When not to use
- The landing page of a site or an app. Use a hero with a heading and a call to action; nothing here is scrollable or indexable.
- A pause menu over a game in progress. Use a system menu or menu tabs, which sit over the play and list more pages.
- Choosing between many entries, saves or levels. Use a list with a selection frame (a save list or a quest log), which scrolls.
- A splash that only shows a logo while something loads. Use a loading screen with progress; this one waits for the player.
- Choosing one of a few answers in a conversation. Use a choice list beside the dialogue box.
You can add this component with the shadcn CLI or by hand:
Into components/title-screen of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/title-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 { TitleScreen } from './title-screen';export function Boot({ onContinue }: { onContinue: () => void }) { return ( <TitleScreen autoplay={false} logo={{ kicker: 'The Song of', name: 'Eventide', tagline: 'Beyond the Far Ridge' }} onChoose={(item) => { if (item.id === 'continue') onContinue(); }} /> );}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 a game opens on. A painted landscape fills it: a lone traveller seen from behind on a rock at a cliff edge, a valley, ranges fading into haze and clouds lit by a low sun, all in vector layers that drift slowly in parallax. On the intro the wordmark sits big in the middle and “Press any button” pulses under it; any key, click, tap or gamepad button starts, and the wordmark glides to the top right while the menu slides in from the right. Continue is preselected, the bright item with a pointer glyph before it; the arrows move the pointer, A or Enter chooses, B or Escape goes back to the intro. Version lines sit in the corner. The screen sits in an original navy bezel with a cyan rim.
- The device is one drawing in design units: --u is min(1px, 100cqi / (960 + 2 × 16)), from the stage’s width by a container query unit, and every size is a multiple of it, so the 960 × 540 screen and its 16-unit bezel scale as a whole. Under 600 px the screen becomes a 392 × 640 portrait one: the painting keeps its height and is cropped around the traveller, and the wordmark, menu and prompt take portrait positions.
- The painting is a single SVG (art.tsx) in layers, back to front: a sky gradient with the sun’s halo and light shafts, a dark sweep and pale streaks of high cloud, cumulus heaps built from arcs (shade, body, and lit heaps toward the sun), three mountain ranges from midpoint-displacement ridges with a fixed seed (so server and browser draw the same), rim-lit and hazier with distance, valley mist, groves and a river, the faceted cliff with grass, and the traveller, lit along his sun side by a warm copy drawn under the dark silhouette.
- Parallax: each layer carries its depth as --k, and one 28 s keyframe pans every layer by --k × 16 units, so far ranges barely move and the cliff and foreground move most. The clouds, mist, birds, halo and rays have their own slower loops, and the whole painting breathes by a slight scale over 48 s.
- Starting flashes the prompt (420 ms), pulses the cyan rim, glides the wordmark from the centre to the top right by translate and scale (760 ms, ease-out), then slides the menu items in from the right 60 ms apart and fades the version lines in.
- The pointer is one glyph in the menu, moved by translate to the selected row on a 30-unit pitch with an overshooting curve (190 ms), and placed just before the item’s words from their measured width (the items are right-aligned and of different lengths). At either end it only nudges; choosing flashes the item and darts the pointer.
- Input: on the intro any key but Tab and the modifiers, a click or tap, or any gamepad button starts; on the menu the arrows or W and S move, A, Enter or Space choose, B, Escape or Backspace go back, a mouse hover moves the pointer and a click chooses. A connected gamepad’s d-pad, stick, A and B work too.
- Uncontrolled, a demo waits on the intro, starts, browses down and up, chooses Continue and backs out, while the screen is on view and untouched; any real input hands it to the reader. Off screen or in a hidden tab every animation pauses.
| Prop | Type | Default | Description |
|---|---|---|---|
items | TitleMenuItem[] | Continue, New Game, Options, Extras | The menu, top to bottom: { id, label }. |
logo | TitleLogo | { kicker: 'The Song of', name: 'Eventide', tagline: 'Beyond the Far Ridge' } | The wordmark: a small kicker line, the name (its size follows its length) and a tagline. |
version | string[] | ['Ver. 1.4.2', 'Data Ver. 3.0', '© 2026 Eventide'] | The lines in the bottom right corner, shown on the menu. |
prompt | string | 'Press any button' | The intro’s call to start. |
selected | number | — | The item under the pointer, controlled, as an index into items. Turns the demo off. |
defaultSelected | number | 0 | Uncontrolled, the item the pointer starts on. |
defaultPhase | 'intro' | 'menu' | 'intro' | Opens on the intro or straight on the menu. Set, it turns the demo off. |
onSelect | (index: number) => void | — | Called when the pointer moves to another item. |
onChoose | (item: TitleMenuItem, index: number) => void | — | Called when an item is chosen. |
onStart | () => void | — | Called when the player presses a button on the intro. |
onBack | () => void | — | Called when the player backs out of the menu to the intro. |
drift | boolean | true | The slow parallax drift of the painting, its clouds and birds. |
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: start, browse, choose, back out. Any real input stops it. |
colorScheme | 'light' | 'dark' | 'system' | — | Sets color-scheme on the root: the bezel’s edge, its shadow and the focus ring follow it; the screen keeps its own colours. |
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 { TitleArt } from './art';import { useAutoplay } from './use-autoplay';import styles from './title-screen.module.css';/* * Title screen: the first screen of a game, on a tablet-like device of its own (a navy bezel with * a cyan rim and small ornaments). A painted sunset landscape (art.tsx) fills the screen: a lone * traveller seen from behind on a rock at a cliff edge, the valley and its ranges beyond. Its * layers pan together by depth, slowly, and the clouds and birds drift on their own loops. * * - Intro: the game's wordmark sits big in the middle and "Press any button" pulses under it. Any/* Title screen (title-screen.tsx): a whole game screen on a device of its own. The bezel is navy with a cyan rim and small cyan ornaments; inside it, the painted landscape (art.tsx), the wordmark, the intro prompt, the menu with its pointer glyph, and the version lines. Every size is a multiple of --u, one design unit (1 px at full size) of a 960 × 540 screen (--sw × --sh) inside a --bz bezel, or 392 × 640 on a narrow stage. --u comes from the stage's width by a container query unit, so the device scales as one drawing; --title-unit fixes it. On the narrow screen the painting keeps its height and is cropped around the traveller. Colours: the screen keeps its colours on any page, as a game screen does; only the bezel's edge, its shadow and the focus ring follow the scheme, through light-dark(). Motion: the painting's layers pan by translate with their depth (--k), the clouds, mist, birds, halo and rays have their own slow loops; the wordmark glides by translate and scale between the intro and the menu; the menu items slide in by keyframes, staggered; the pointer slides by translate with an overshoot. Off screen everything pauses; reduced motion stops it all. */.root { /* The screen. */ --ink: #f6f3e9; --ink-dim: rgb(226 230 228 / 0.9); --mark: #f3eedf; --logo: #efe8d6; --logo-shadow: rgb(54 34 18 / 0.42);// Copyright (c) 2026 Bruno Delorence. MIT License./* * The title screen's painting: a lone traveller, seen from behind, on a rock at a cliff edge over * a wide valley at sunset. One SVG in a 960 × 540 drawing, built from flat shapes and gradients in * layers, back to front: the sky and the sun's halo, two banks of cloud, three mountain ranges * hazier with distance, mist in the valley, the near hills with a river, the cliff and the * traveller, the foreground grass, and a few birds. * * Each layer is a <g> with a depth (--k): the CSS pans them together, far ones barely and near ones * most, which reads as a slow camera drift. Clouds and birds have their own slower loops. Ridges * are midpoint-displacement lines from a fixed seed, so the server and the browser draw the same * mountains. Every layer is drawn wider than the frame, so a pan never shows an edge. */import type { CSSProperties } from 'react';/** A small seeded random: the same numbers on every render. */function seeded(seed: number) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };// 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 labelled group named after the game; the intro is a real button (“Press any button”), and the menu is a role="menu" of menuitem buttons with a roving tabindex, so Tab reaches the selected item and the arrows move along.
- Focus follows the screen while it is inside: to the selected item when the menu opens and as the pointer moves, back to the start button on the intro. A focus ring rings the device, and a focused item gets its own outline.
- A polite live region says “Main menu. Continue.” on start, each item as the pointer reaches it, “… chosen.” on a choice, and the prompt again on going back.
- The painting, the pointer glyph, the ornaments and the flashes are aria-hidden; the wordmark is text.
- Under reduced motion there is no drift, no demo and no slides: the states swap at once. Every animation pauses off screen or in a hidden tab, and drift={false} stills the painting.