Gamestage UI

Components for the interface a live game wears: choices, score, timer, rewards and the leaderboard.

Gamestage UI is a component library for the interface a live game wears: the choices, the score, the timer, the rounds, the streaks, the rewards and the leaderboard. It renders the game around the action rather than owning the action, so the video, the 3D scene or the canvas stays yours.

Components are copied into your project rather than installed as a dependency. You get the source, you edit it, and nothing upgrades under you.

Install a component

Gamestage UI serves from a private registry, so it needs a token. The token is a GitHub token with read access to gamestageai/gamestage-ui, and gh auth token prints one.

export GAMESTAGE_REGISTRY_TOKEN=$(gh auth token)
npx gamestage-ui add choice

That writes the component and its stylesheet into components/gamestage/, brings in whatever it depends on, and installs any npm packages it needs with the package manager your lockfile names. A file that is already there is left alone unless you pass --overwrite, and the command prints every path it wrote.

If you get "not found", check the token before you hunt the component. A request without one returns 404 rather than 403, so a missing token looks exactly like a missing component.

CommandWhat it does
npx gamestage-ui add <name...>Copies components in, with what they depend on
npx gamestage-ui listPrints every component the registry serves
npx gamestage-ui initWrites gamestage.json, which pins the registry URL

init is optional. The registry URL is built in, and gamestage.json matters only when you want to point at a different one. The token is never written to a file.

A question on screen

import { useState } from 'react'
import { Choice, type ChoiceResult } from '@/components/gamestage/choice'

export function Question() {
  const [result, setResult] = useState<ChoiceResult>()

  return (
    <Choice
      label="Which driver led the most laps?"
      presentation="buttons"
      commitOnSelect
      result={result}
      options={[
        { id: 'ver', label: 'Verstappen' },
        { id: 'nor', label: 'Norris' },
        { id: 'lec', label: 'Leclerc' },
      ]}
      onCommitted={(ids) => setResult({ [ids[0]]: ids[0] === 'ver' ? 'correct' : 'wrong' })}
    />
  )
}

That renders three answers, records the pick and shows the outcome you hand back. The same component renders as cards, a list, a wheel or an overlay over a live scene by changing presentation, because a quiz grid and a card picker are one choice wearing different clothes.

What it will not do

This is the part that saves time later. Gamestage UI performs none of these and will not start.

It does notYou do
Sign anyone inPass the player you already have
Store progressPersist it wherever you keep state
Call your APICall it from the callbacks
Decide who wonSettle the outcome and pass it back
Ask for consentRender your own consent surface
Send analyticsForward the callbacks to your own tracking

The boundary is enforced rather than promised: no component imports anything but React, and a gate check fails the build if one reaches for fetch, storage, a socket or a cookie.

This is why it sits beside the Engine rather than inside it. Gamestage's Engine decides whether a play was any good and holds the score. Gamestage UI draws what the Engine decided. Neither knows about the other, and you can use either on its own.

Theming

Every colour, size, duration and typeface is a token. Components read tokens and never define their own, so changing a token changes every surface at once.

<html data-gs-theme="arcade" data-gs-context="light">

A theme changes the palette, the typefaces, the corner radius, the edge treatment, and what a control does when you press it: travel on an offset base, scale, or hold still and shift colour instead. Three themes ship today: arcade, paper and stadium.

Selecting on it from outside

Components describe themselves with data-gs-* attributes, so your code and your tests find things without depending on class names or on the words on screen. A theme changes the first and a translation changes the second.

[data-gs-part="label"]                       /* every label, everywhere */
[data-gs-scope="stat"][data-gs-part="label"] /* only a stat's */
[data-gs-state="wrong"]                      /* a settled wrong answer */

Those attributes are public API. One is documented before it is emitted, and a gate check fails the build when the documentation and the components disagree.

What the registry serves

Grouped by what it's for. The groups are the library's own: each item declares its category, and the Gallery at ui.gamestage.ai reads the same field, so what you see there and what you read here cannot disagree. New items ship often; treat this as a sample rather than a count.

GroupItems
Controlsbutton, choice, selector, slider, toggle
Playeridentity, player-card, presence, profile
Game statehud, lives, multiplier, round, score, stat, streak, timer
Progresslevel-ring, milestones, progress-bar, quest, segments
Rewardsinventory, reward
Competitionleaderboard, podium, versus
Chartschart (bar, line, area, radial)
Flow and guidancecoach-mark, onboarding, tip, tutorial-step
Mediamedia-frame, story
Game feelfeedback
Compositionnav, stage
Application surfacescode-entry, consent, discovery, empty-state, error-state, field, form, login, prize-entry, recovery-code, sponsor
Foundationsadapter, cues, fonts, fonts-arcade, fonts-paper, theme-arcade, theme-paper, theme-stadium, tokens

glyphs is in the registry, ungrouped.

You rarely name the last two rows. Application surfaces are whole screens rather than parts of one, and Foundations are the tokens, typefaces, themes and the host adapter that other items pull in for themselves, so asking for choice brings what it needs without you listing it.

npx gamestage-ui list prints what the registry holds today, which is the list to trust over this one.

Three things arrived in the registry on 28 September. nav takes presentation="glass" and labels="hidden" for an icon-only bar. story takes gestures (tap, swipe across, swipe down to close) and fullscreen. And every pressable control presses down under a thumb on an iPhone and never selects its words.