Game toasts
NewA game’s notices at the side of the screen: quest started, quest updated, a tip, an item picked up, each sliding in, holding and fading.
game-toasts/game-toasts.tsxThe 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
- Telling the player, mid-play, that something just changed: a quest began or moved on, a control was learned, an item was picked up.
- Notices the player glances at and keeps playing: each leaves on its own after
hold, and the column pauses while it is pointed at or focused. - Pickups that come in bursts: a counter toast with a stable
idcounts on (three coins are one toast that bumps to 3) instead of stacking three. - Pointing to where to read more: a
whereline with a + button that opens that page of the menu (onOpen). - A one-line system confirmation apart from the column, such as “Progress saved”: a
noticeband in the middle.
When not to use
- Anything the player must answer or confirm before going on. Use a game modal or a dialog box that holds the game.
- Lines a character speaks. Use a dialog box with the speaker’s name, paced by the player.
- State that must stay on screen, such as coins held or health. Use a HUD counter or meter; a toast leaves.
- Errors and notices in an app or on a website. Use the product’s own toast or an inline message, with plain type and a close button.
- A long list of what happened, to be read later. Use a quest log or a journal page; the column keeps only the last
max.
You can add this component with the shadcn CLI or by hand:
Into components/game-toasts of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/game-toasts.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 { useState } from 'react';import { GameToasts, type GameToast } from './game-toasts';let next = 0;export function QuestNotices() { const [toasts, setToasts] = useState<GameToast[]>([]); const push = (toast: GameToast) => setToasts((now) => [...now, { id: `n${next++}`, ...toast }]); return ( <> <button onClick={() => push({ title: 'The Lantern Keeper', where: 'Journal' })}> Start quest </button> <GameToasts bare toasts={toasts} onDismiss={(id) => setToasts((now) => now.filter((toast) => toast.id !== id))} onOpen={(toast) => console.log('open', toast.where)} /> </> );}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 column of short notices an adventure game shows while you play: a quest that began or moved on, a control just learned with its key phrase in red, a count of something picked up, an item with its icon, each with where to read more in the menu and a small + to open it. They slide in under one another, hold a few seconds (paused while you point at or focus them), fade, and the rest close up. Picking up more of the same thing counts on in one toast. Send each kind from the chips, or press a +.
- Each toast is a dark plate that fades to nothing on its right: a gradient from the plate colour, solid for the first 10 %, to transparent at the right end, with round ends. Every size is a multiple of --u, one design unit (a 22 u plate, a 15 u italic title, a 14 u + disc), which the root sets from its own width with a container query unit, up to 1.4 px, so the column scales as one.
- Kinds share that plate: a line (one title, cut with an ellipsis), a tip (up to three lines with one phrase drawn in red), a counter (a short plate with an emblem and a number), and a notice, a wide band with a thin light rim apart from the column. Any toast can carry a small yellow tag in the gap above it (“Updated”), an icon overlapping the plate’s left end, and a where line under its right side with the + button.
- The rows follow the list shown (the last max toasts, and the latest notice). A newcomer’s plate slides 36 u in from the right with a slight overshoot in 300 ms, its where line follows 110 ms later, and its tag and icon pop in. A toast with an id already on screen updates it: a counter’s number remounts and bumps from gold, its emblem glints, and its clock restarts.
- Each toast has a clock of hold ms (2.6 s for a notice). All clocks pause while the pointer or the focus is on the column, keeping what is left, and resume after. When one runs out, the toast is asked away (onDismiss, or the own list), fades out drifting right in 420 ms, and is then removed: the rows under it are measured before and after, and glide up from where they were by FLIP with the Web Animations API.
- The + is a real button: pressing it remounts its disc, which dips and springs back as a ring of light spreads, calls onOpen, and the toast leaves 240 ms later, as a game would open that page.
- The plates carry their own dark ground, so they read on any page. What sits straight on the stage (the where line, its disc, the tag) is dark ink on a light page and the game’s light ink with a soft shadow on a dark one, and on a light page the plate stays solid longer before it fades, so the end of a long title keeps its dark ground, while on a dark page the plate is a step lighter than the page, so its edge still shows. All of it is light-dark() tokens on the root.
- Uncontrolled, it keeps its own list, with chips under it to send each kind, and a demo plays while it is on screen and untouched: a quest, a tip, three coins counted in one toast, an update opened with its +, an item, a notice. Given toasts, it shows those and asks onDismiss to remove one.
| Prop | Type | Default | Description |
|---|---|---|---|
toasts | GameToast[] | — | The toasts shown, oldest first (controlled). A toast is { id?, kind?, title, highlight?, tag?, where?, icon?, count?, hold? }: kind is line, tip, counter or notice; icon is emblem, mushroom, fruit or crystal. A toast with an id already shown updates it (a counter counts on). Without it, the component keeps its own list, with chips and a demo. |
onDismiss | (id: string) => void | — | Asked to remove a toast: its time is up, its + was pressed, or Escape cleared the column. Controlled, remove it from toasts. |
onOpen | (toast: GameToast) => void | — | Called when a toast’s + is pressed: open the menu page it names. |
hold | number | 4200 | How long a toast stays, in ms, unless it sets its own. A notice holds 2600. |
max | number | 5 | How many toasts the column shows; past it, the oldest leaves early. |
bare | boolean | false | The column and band alone, without the room kept for them and the chips, to lay over a game of your own. Sizes follow --toasts-unit (default up to 1.4 px). |
autoplay | boolean | true | While on screen and untouched, a short run of play sends toasts and opens one. Uncontrolled only. |
colorScheme | 'light' | 'dark' | 'system' | — | Forces light or dark for what sits on the page (where lines, discs, tags, band, chips); by default it follows the page. The plates keep their dark ground. |
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 { Fragment, useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useState, type KeyboardEvent,} from 'react';import { Icon, type ToastIcon } from './icons';import { useAutoplay } from './use-autoplay';import styles from './game-toasts.module.css';/* * Game toasts: the column of short notices at the right of an adventure game's screen. Each says * what just changed (a quest began or moved on, a control was learned, something was picked up) * and, under it, where in the menu to read more, with a small + button that opens that page. * The player glances and keeps playing; the notice leaves on its own. *// Copyright (c) 2026 Bruno Delorence. MIT License./* * The toasts' item art: small flat drawings in a 32-unit box, bold and a little glossy, the way a * game's inventory icons read at a glance. They are decorative (the toast's text names the item), * so each is aria-hidden and sized by its parent in CSS. */export type ToastIcon = 'emblem' | 'mushroom' | 'fruit' | 'crystal';export function Icon({ name }: { name: ToastIcon }) { return ( <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"> {name === 'emblem' && ( // A gold seal: a four-point star over a ring of small rays, with a bright facet. <g> <path d="M16 3.2l2.3 7.6 7.5-2.4-4.8 6.1 6.5 4.6-7.9.4.4 8L16 21.4l-4 6.1.4-8-7.9-.4 6.5-4.6-4.8-6.1 7.5 2.4z" fill="#c8920c" /> <path d="M16 5.6l1.9 7.4 7.4 1.9-7.4 1.9L16 24.2l-1.9-7.4-7.4-1.9 7.4-1.9z" fill="#fcc413" />/* Game toasts (game-toasts.tsx): the notices' column and the system band, alone on the page's stage, with a row of chips below to send each kind. Every size is a multiple of --u, one design unit: a toast's plate is 22 u tall with a 15 u italic title, a tip's lines are 20 u apart, the + disc is 14 u. The root sets --u from its own width (a container query unit), up to 1.4 px, so the whole column scales as one; --toasts-unit fixes it. The plates carry their own dark ground: a game's toast is a dark plate that fades out to its right, and it reads the same over a light page or a dark one. What sits straight on the stage (the where line, its disc, the tag) follows the colour scheme through light-dark(): dark ink on a light page, the game's light ink with a soft shadow on a dark one. On a light page the plate also stays solid longer before it fades, so the end of a long title keeps its dark ground; on a dark page it is a step lighter than the page, so its edge still shows. Motion: arrivals and leavers are keyframes on their row; the where line, a tag and an icon follow with their own; a counter's bump, an icon's glint, the disc's dip and ring are keyframes on parts remounted per event; rows that close a gap glide by a FLIP the script plays with the Web Animations API. */.root { /* The plates. */ --plate: rgb(42 41 38); --plate-mid: rgb(42 41 38 / 0.5);// 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 column is an ordered list labelled Notifications, and the band a list labelled System messages; a polite live region reads each new toast (“Updated: A light in the tower. Journal.”) and a counter’s new count.
- Each + is a real button named by its page and toast (“Open Journal: The Lantern Keeper”), with a hit area larger than its 14 u disc; the icons and the emblem are aria-hidden.
- Hovering or focusing the column pauses every toast’s clock, so nothing disappears while it is being read or reached (WCAG 2.2.1); Escape clears the column.
- Leaving toasts are hidden from assistive technology and their buttons disabled at once.
- The chips are buttons in a labelled group; everything shows a :focus-visible ring.
- Reduced motion: toasts and the band only fade; no slides, pops, bumps, rings or glides. The demo never plays.