Modal
<Modal> draws a dialog over the whole screen. Put one at any level, including inside an Entity: like a Hud, it draws in the screen layer and keeps every context around it.
Its props are in ModalProps.
import { useState } from "react";import { Button, Modal, Text } from "@spawnite/engine";
export function Chest() { const [open, setOpen] = useState(true);
return ( <Modal open={open} title="A chest" pause onClose={() => setOpen(false)} actions={<Button onPress={() => setOpen(false)}>Take</Button>} > <Text>Ten coins inside.</Text> </Modal> );}The dialog centres in the screen less the edges the screen layer keeps clear of, the device’s safe area or, in the player app, the app’s own controls, so a pane’s title never sits under the app’s pill. The screen layer names the variables.
The pane
Section titled “The pane”The pane is glass by default, with the title at its top. variant={ModalVariant.Bare} draws no pane: the children draw their own, as an end screen with its own art does, and the title names the dialog for a screen reader alone. className restyles the pane over the variant, its colours, its padding or its alignment. dialogClassName goes on the dialog around the pane and the buttons: the backdrop’s utilities, such as backdrop:bg-black/70, and what the pane and the buttons share, such as items-stretch for buttons as wide as the pane. classNames names every part, dialog, pane, title, overline and actions, as the theme page describes. A game reaches the pane through these props, never through the dialog’s markup.
import { Button, Modal, ModalVariant, Text } from "@spawnite/engine";
export function PauseMenu({ open, onResume,}: { open: boolean; onResume: () => void;}) { return ( <Modal open={open} title="Paused" variant={ModalVariant.Glass} className="items-stretch bg-neutral-900/95 text-left" dialogClassName="backdrop:bg-black/70 backdrop:backdrop-blur-xs" actions={<Button onPress={onResume}>Resume</Button>} > <Text>The field waits.</Text> </Modal> );}Buttons below the pane
Section titled “Buttons below the pane”The modal’s children fill its glass pane, under the title, which it draws as a level 2 Heading. An overline puts a small line above the title that says where the player is, as the round screen names the level. Pass the buttons as actions instead, and they stand in a row below the pane, outside it. A player on a phone reaches them with a thumb, and the pane holds only what to read. On a screen too short for both, such as a phone held landscape, the pane shrinks and scrolls and the row keeps its height, so the buttons stay in view.
Godot’s AcceptDialog and Unity’s dialogs draw their buttons inside the window. Mobile games such as Clash Royale and Brawl Stars stand the main button under the card, and the engine follows them. The buttons stay inside the dialog, so focus stays with them, and a click beside them is a click on the backdrop.
One stack
Section titled “One stack”The engine keeps one stack of open modals, and only the top one draws. Two chests opened at once show one; closing it shows the other. The world stays paused while any modal on the stack asks for pause, the top one or one under it.
Pausing
Section titled “Pausing”A modal pauses nothing unless it passes pause. The world keeps running behind it, as it does behind a Roblox menu: a game with other players in it cannot stop for one player’s dialog. A single-player game passes pause, as Godot’s get_tree().paused does, so the world waits while the player reads. Time spent paused is not made up afterwards.
A pause holds the world’s steps and every view’s clock. While a pausing modal is open, each useFrame callback gets zero seconds and the canvas clock’s elapsed time stands still, so a game’s own animations, particles and shader effects freeze behind the modal, as Unity’s zero Time.timeScale freezes them. The modal and the rest of the game’s UI are HTML, so they keep animating. A view that must keep moving behind the modal, such as a menu drawn in the canvas, calls useUnscaledFrame from @spawnite/engine instead of useFrame. It takes the same callback and priority, and hands the callback the wall clock’s seconds, as Unity’s Time.unscaledDeltaTime and Godot’s PROCESS_MODE_ALWAYS do. The engine’s camera controls, its free camera and its frame counter keep the wall clock too. In a room a modal pauses nothing, so every view keeps the wall clock.
The cursor
Section titled “The cursor”A modal frees the cursor while it is open, so its buttons take a click under a camera that captures the cursor, as in a shooter, and the camera takes the cursor back when it closes. Roblox frees the mouse for a visible Modal button and Unreal’s menus switch to UI input the same way. freeCursor={false} keeps the cursor captured, for a modal with nothing to click that a player reads without losing the mouse-look. The camera page says what the player sees and how the lock comes back.
Escape and the backdrop
Section titled “Escape and the backdrop”The modal is the browser’s own dialog, so focus stays inside it and the page behind it takes no input. A click on the backdrop calls onClose rather than closing the dialog, because the stack, not the browser, decides what is open. Escape does not close a modal inside a game: the engine’s menu takes Escape before any dialog and opens over the modal, so a player is never stuck behind one.