Build a game settings menu in React
A game settings menu is tabs of rows: a setting's name on the left, its control on the right, and a footer that applies the changes. This guide builds one with zeroUI on shadcn, or installs the finished block with one command.
Start from the block
The quickest way is the Settings menu block: one file in components/blocks/ with every piece below already wired. Installing it brings the components it uses.
pnpm dlx shadcn@latest registry add @zeroui=https://zeroui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zeroui/settings-menuThe rest of this page explains how it is put together, so you can change it or build your own.
One setting, one row
Players scan a settings screen down the left edge, reading names, and reach for controls on the right. So each setting is a two-column row: the name with an optional line of explanation, then the control, pushed to the end. On a phone the columns stack.
function Row({ name, line, children }) { return ( <div className="grid items-center gap-x-8 gap-y-3 py-4 sm:grid-cols-[1fr_minmax(0,16rem)]"> <div className="grid gap-0.5"> <span className="font-bold font-zero italic">{name}</span> {line ? <span className="text-zero-sm text-zero-text-3">{line}</span> : null} </div> <div className="flex items-center sm:justify-end">{children}</div> </div> );}The explanation is where a setting earns its place: "Stops tearing; adds a little delay" tells a player what vertical sync costs, which the name alone does not.
Tabs for each kind of setting
Group the rows by what they change (display, audio, controls, gameplay) and put each group in a Tabs panel. Volumes are Sliders, on and off are Switches, a choice from a list is a Select, and a choice of three is a Radio group. Give every control an aria-label: the row's name is not its label.
<Tabs defaultValue="display"> <TabsList> <TabsTab value="display">Display</TabsTab> <TabsTab value="audio">Audio</TabsTab> <TabsTab value="controls">Controls</TabsTab> </TabsList> <TabsPanel value="audio"> <Row name="Music"> <Slider value={music} onValueChange={(v) => setMusic(v as number)} aria-label="Music volume" /> </Row> <Row name="Subtitles"> <Switch checked={subtitles} onCheckedChange={setSubtitles} aria-label="Subtitles" /> </Row> </TabsPanel></Tabs>Key bindings read best as keys: a row per action, its keys drawn with Kbd.
Apply only what changed
Keep two copies: what is on screen and what was last applied. The Apply button stays disabled until they differ, so a player can see at a glance whether anything is waiting.
const [settings, setSettings] = useState(defaults);const [saved, setSaved] = useState(defaults);const changed = JSON.stringify(settings) !== JSON.stringify(saved);<Button disabled={!changed} onClick={() => setSaved(settings)}> {changed ? "Apply" : "Applied"}</Button>Make restoring the defaults deliberate
Restoring defaults wipes everything a player tuned, so it should not happen on a stray click. A Hold button has to be held while a fill runs across it. Released early, it drains back. It works with the pointer, with Enter or Space, or with a hotkey, and screen readers are told to press and hold.
<HoldButton size="sm" onConfirm={() => setSettings(defaults)}> Hold to restore defaults</HoldButton>Where to go next
For the rest of a game's screens, see the blocks: a mission briefing, a loadout, a leaderboard and a HUD. To change the look of all of them at once, read Theme zeroUI with its tokens.