
Game UI: HUD, Feedback & Rewards
The SDK ships the platform's own game UI: a HUD bar, feedback flashes, and flying reward icons. Use it instead of building your own, so every Game feels like part of the same platform.
What's in the Toolkit
Three UI systems live in a separate entry point, @minit-games/sdk/ui:
- Header bar: the standard HUD at the top of the screen, holding stat panels like Score, Turns, or Lives.
- Feedback pop-ups: short, punchy text flashes ("Combo x3!", "Life Lost") that float over gameplay and auto-dismiss.
- Flying rewards: icons that fly from a position in your game to a HUD panel when the player earns something.
All three are pure DOM. They need no rendering library, make no network requests (fonts are bundled inside the package), and work on top of any canvas, WebGL context, or DOM game.
Warning: These helpers are npm-package only. Unlike
loadingDoneorreportResult, they have nowindow.minitequivalent and are not exposed by the Unity, Godot, Defold, or PlayCanvas facades. If your game has no build step and talks towindow.minitdirectly, you cannot use them.
Import
import {
createHeaderBar,
getHeaderBar,
showPositiveFeedback,
showNeutralFeedback,
showNegativeFeedback,
showFeedback,
preloadFeedbackFont,
spawnReward,
spawnRewards,
} from '@minit-games/sdk/ui'
Note the /ui suffix: these are not exported from the main @minit-games/sdk entry point.
Feedback Pop-ups
A feedback pop-up is a large piece of text that appears centred over the game, holds for about a second, and fades out. It never blocks input, so it can fire mid-gameplay without interrupting the player.
showPositiveFeedback('Combo x3!') // green
showNeutralFeedback('x2 Speed') // orange
showNegativeFeedback('Life Lost') // red
Each wrapper takes an optional duration in milliseconds:
showPositiveFeedback('Level Up!', 1500)
Or call the base function directly with an explicit variant:
showFeedback(text, variant, duration)
text(string, required): the label. One to three words.variant("positive" | "neutral" | "negative", optional, default"neutral"): the colour treatment.duration(number, optional, default1000): visible time in milliseconds before the fade-out starts.
Long text is scaled down automatically to fit within 85% of the screen width, so a pop-up never runs off the edge, but short text always reads better.
Which Variant to Use
| Moment | Variant | Example text |
|---|---|---|
| Score, combo, bonus collected, level cleared, win | positive (green) | "Combo x3!", "+50", "Level Up!" |
| Modifier activated, streak reset, non-fatal warning | neutral (orange) | "x2 Speed", "Streak Lost", "10s Left!" |
| Life lost, mistake, penalty, time up, fail | negative (red) | "Life Lost", "Wrong!", "Time Up!" |
Rules of Thumb
- Fire on every moment with clear emotional weight. If the player will feel it, flash it.
- Fire at the moment it happens, not delayed, not bundled up and shown later.
- Never silently subtract health, lives, or score. Always pair the loss with a negative flash.
- Don't fire on micro-actions: every tile flip, every step of movement. Pop-ups lose all meaning if they never stop.
- Keep text short and punchy: one to three words, no punctuation except
!for emphasis.
Avoiding the First-Use Font Flash
Preload the pop-up font during startup so the very first pop-up renders in the right typeface:
import { initializeSDK } from '@minit-games/sdk'
import { preloadFeedbackFont } from '@minit-games/sdk/ui'
initializeSDK()
await preloadFeedbackFont() // optional; safe to call more than once
This is optional. Skipping it only risks a brief fallback-font flash on the first pop-up.
Header Bar
The header bar is the standard HUD across Games. Create it once at startup, then add one panel per stat.
const header = createHeaderBar({ y: 60, padding: 40 })
const turns = header.addPanel({ label: 'Turns', value: 10 }) // left (default)
const score = header.addPanel({ label: 'Score', value: 0, align: 'right' })
score.setValue(120, { animate: true })
turns.setValue(9)
createHeaderBar(config?)
| Option | Type | Default | Purpose |
|---|---|---|---|
y | number | 60 | Distance from the top of the screen, in pixels. Your primary positioning knob. |
padding | number | 75 | Side inset, in pixels. |
width | number | none | Constrain the bar to a fixed width, centred. |
layout | "split" | "even" | "split" | "split" groups panels left vs right by their align; "even" spreads all panels evenly across the bar. |
container | HTMLElement | none | Append the bar to your game wrapper (absolute positioning) instead of the page (fixed positioning). Use this when your game is a scaled canvas, so the HUD scales with it. |
zIndex | number | 9000 | Stacking order. |
defaultStyle | PanelStyle | none | Colour/size overrides applied to every panel. Omit by default. |
Only one header bar exists at a time: calling createHeaderBar again destroys the previous bar and its panels. Use getHeaderBar() to reach the current instance from elsewhere in your code; it returns null if none has been created.
header.addPanel(config)
| Option | Type | Default | Purpose |
|---|---|---|---|
value | number | string | none | Required. The initial value shown. |
label | string | none | Plain-text caption above the value ('Score', 'Turns'). |
icon | string | none | A single character shown instead of a label. Prefer label. |
align | "left" | "right" | "left" | Which group the panel joins in "split" layout. |
onClick | () => void | none | Tap handler for the panel. |
style | PanelStyle | none | Per-panel colour/size overrides. Omit by default. |
addPanel returns a panel handle:
| Method | What it does |
|---|---|
setValue(value, { animate?, duration? }) | Update the displayed value. animate: true counts up to the new number. |
getValue() | Read the current value. |
getPosition() | The panel's { x, y }, the target for flying rewards. |
flyToPanel(options) | Fly a single reward icon from a position in your game to this panel (see below). |
setLabel(text) | Change the label or icon. |
setVisible(visible) | Show or hide the panel. |
destroy() | Remove just this panel. |
Header Bar Conventions
Treat the header as layout only:
- Position with
yandpadding: adjust those, not font sizes. - Score goes on the right (
align: 'right'). Secondary stats (turns, moves, lives) go on the left. - Use
layout: 'even'when the stats should span the whole bar instead of grouping left and right. - Don't customise size or colour. Omit
style,defaultStyle,labelSize,valueSize, and all colour fields unless you specifically want a different look; the SDK ships fixed styling so games feel consistent. - Use plain-text labels, not emoji. Prefer
label: 'Score'over theiconfield.
Flying Rewards
When the player earns something at a specific place on screen, fly an icon from that place to the HUD panel that tracks it. The rule is one icon per point earned, not one icon per scoring event.
+1: panel.flyToPanel(...)
score.flyToPanel({
start: { x: tile.x, y: tile.y },
onArrive: () => score.setValue(Number(score.getValue()) + 1, { animate: true }),
})
| Option | Type | Default | Purpose |
|---|---|---|---|
start | { x, y } | none | Required. Where the icon spawns: the position of the in-game event. |
onArrive | () => void | none | Fires when the icon lands. Bump the panel value here, not before. |
size | number | 40 | Icon size in pixels. |
scale | number | 1.0 | Multiplier applied to size. |
delay | number | 0 | Milliseconds to wait before starting. |
visual | string | { type: 'image', src } | { type: 'color', color } | orange circle | Icon appearance. Omit by default. |
container | HTMLElement | none | Pass your scaled game wrapper so the icon scales with the game. |
+N: spawnRewards(count, options, staggerMs?)
For a payout of more than one point, spawnRewards spawns the whole payout with a staggered trickle:
spawnRewards(12, {
start: { x: chest.x, y: chest.y },
target: score.getPosition(),
onAllArrive: () => score.setValue(Number(score.getValue()) + 12, { animate: true }),
})
count: points earned. One icon per point for counts of 1–5.options: the same options asspawnReward(below), exceptonArriveis replaced byonAllArrive, which fires once after the last icon lands.staggerMs(optional, default50): delay between each icon's spawn.
When count is greater than 5, icons cluster into larger denominations (125 / 25 / 5 / 1) with proportionally bigger icons, so a 500-point payout reads clearly instead of flooding the screen with 500 circles.
Note that spawnRewards needs an explicit target. Pass panel.getPosition().
Low-level: spawnReward(options)
spawnReward is the single-icon primitive both of the above build on. Reach for it only when neither fits, for example flying an icon somewhere that isn't a HUD panel.
| Option | Type | Default |
|---|---|---|
start | { x, y } | required |
target | { x, y } | required |
visual | { type: 'emoji', emoji } | { type: 'image', src } | { type: 'color', color } | orange circle |
size | number | 60 |
scale | number | 1.0 |
holdDuration | number | 350 (pause at the scatter position, in ms) |
scatterDistance | number | 30 (how far icons scatter from start, in px) |
flyDuration | number | 400 (flight time, in ms) |
onArrive | () => void | none |
zIndex | number | 10000 |
container | HTMLElement | none |
When to Skip the Animation
Fly icons when there is a meaningful source position on screen. Skip it (and just call setValue) for a passive time bonus, an off-screen award, or anywhere instant feedback reads better.
Putting It Together
import { initializeSDK, reportResult, loadingDone } from '@minit-games/sdk'
import {
createHeaderBar,
preloadFeedbackFont,
showPositiveFeedback,
showNegativeFeedback,
spawnRewards,
} from '@minit-games/sdk/ui'
initializeSDK()
await preloadFeedbackFont()
const header = createHeaderBar({ y: 60, padding: 40 })
const lives = header.addPanel({ label: 'Lives', value: 3 })
const score = header.addPanel({ label: 'Score', value: 0, align: 'right' })
loadingDone()
function onGemCollected(gem, points) {
showPositiveFeedback(`+${points}`)
spawnRewards(points, {
start: { x: gem.x, y: gem.y },
target: score.getPosition(),
onAllArrive: () =>
score.setValue(Number(score.getValue()) + points, { animate: true }),
})
}
function onHit() {
showNegativeFeedback('Life Lost') // never subtract silently
lives.setValue(Number(lives.getValue()) - 1)
if (Number(lives.getValue()) <= 0) {
reportResult(Number(score.getValue()), { flavorText: 'Ran out of lives on wave 4' })
}
}
Bundle Size
The toolkit's fonts are inlined as base64 woff2 and tree-shaken per module, so your game only pays for what it imports:
| What you import | Font | Added to bundle |
|---|---|---|
createHeaderBar | Lato 400 + 700 | ~37 KB base64 (~27.5 KB woff2) |
| any feedback function | Bowlby One SC 400 | ~26 KB base64 (~19.4 KB woff2) |
| neither | none | 0 KB |
Per-module elimination applies when you build with a tree-shaking bundler (Vite, Rollup, esbuild, webpack). Both font families ship under the SIL Open Font License; their licence texts are included in the npm package.
Testing Locally
The toolkit has no host dependency: it renders identically in npm run dev, the Creator Console preview, and the app. Open your game in a mobile-sized browser viewport; no URL params or host stubs needed.
Deeper Dives
- Install the Minit SDK: installing the package and the core API
- Interactive Tutorials: the other half of
@minit-games/sdk/ui, first-play onboarding - Reporting Results: ending the run, and what
flavorTextis for - Using Fonts in Your Game: bundling your game's own fonts