Skip to content

Best Practices

Critical topics and recommended patterns to follow when building mini-apps.


Screen Cutout (Notch / Dynamic Island)

Many iPhones and Android devices have a front-camera cutout at the top (notch, Dynamic Island, punch-hole) and a home indicator at the bottom. When a mini-app opens full-screen, these areas can overlap game UI elements.

The Golden Rule

Let the background bleed full-screen. Keep interactive UI inside the safe zone.

┌─────────────────────────────┐
│   [ Dynamic Island ]        │  ← Background extends here (full-screen feel)
│─────────────────────────────│
│  ❤️❤️❤️        SCORE: 500  │  ← UI elements start below the safe area
│                             │
│      GAME AREA              │
│                             │
│─────────────────────────────│
│        ▬ (swipe bar)        │  ← Background extends here too
└─────────────────────────────┘

Device Reference Values

Devicetopbottom
iPhone 16 / 15 Pro (Dynamic Island)59dp34dp
iPhone 13 / 14 (notch)47dp34dp
iPhone SE / 8 (physical home)20dp0dp
Android gesture nav + cutout27–40dp24–48dp
Android gesture nav, no cutout0dp24–48dp

The platform automatically injects CSS custom properties when the WebView loads:

css
/* Background is full-screen — extends behind the notch */
body {
  background: #1a1a2e;
  /* Do NOT add padding here — let the background bleed */
}

/* Apply safe area only to UI containers */
.game-hud {
  padding-top: var(--sa-top, 0px);
  padding-bottom: var(--sa-bottom, 0px);
  padding-left: var(--sa-left, 0px);
  padding-right: var(--sa-right, 0px);
}

.score-panel {
  margin-top: var(--sa-top, 0px);
}

.bottom-bar {
  padding-bottom: calc(var(--sa-bottom, 0px) + 8px);
}

JS Usage

js
const insets = supergame.getSafeAreaInsets()
// { top: 59, bottom: 34, left: 0, right: 0 }

// Manual layout in canvas-based games
if (insets.top > 0) {
  uiCamera.y = insets.top
}
if (insets.bottom > 0) {
  bottomBar.y = screenHeight - insets.bottom - bottomBar.height
}

// Check if device has a notch
const hasNotch = insets.top > 20

Wrong ❌

css
/* WRONG: padding on body clips the background, leaving a white gap */
body {
  padding-top: var(--sa-top, 0px); /* ❌ */
}

Correct ✅

css
/* CORRECT: body is full-screen; only the content container gets insets */
body {
  background: #1a1a2e; /* bleeds to edges */
}
.hud {
  padding-top: var(--sa-top, 0px); /* ✅ content shifts down */
}

Lifecycle: Background / Foreground

When the mini-app is scrolled off-screen in the Discovery feed or sent to background via the overlay menu, the platform automatically:

  • Pauses all <audio> and <video> elements in the WebView
  • Stops gyroscope / accelerometer streams
  • Keeps multiplayer (WebSocket) connections alive

However, pausing/resuming the game loop and restarting background music is the developer's responsibility.

Music / Audio Management

Important

The platform pauses audio elements automatically but does not resume them. You must call play() yourself inside onForeground.

js
const bgMusic = new Audio('music.mp3')
bgMusic.loop = true
bgMusic.play()

supergame.onBackground(() => {
  // Platform already paused the audio element
  // Stop your game loop:
  gameLoop.stop()
})

supergame.onForeground(() => {
  // Restart music (platform does not do this)
  bgMusic.play().catch(() => {})
  // Resume game loop
  gameLoop.start()
})
csharp
void Start()
{
    SuperGame.onBackground(() => {
        bgMusicSource.Pause();
        Time.timeScale = 0f;
    });

    SuperGame.onForeground(() => {
        bgMusicSource.Play();
        Time.timeScale = 1f;
    });
}
gdscript
func _ready():
    gameTegra.onBackground(func():
        get_tree().paused = true
        $BGMusic.stream_paused = true
    )
    gameTegra.onForeground(func():
        get_tree().paused = false
        $BGMusic.stream_paused = false
    )

Multiplayer Stays Alive

Sensor streams are paused but multiplayer connections remain active. Sync any missed events when the player returns:

js
supergame.onForeground(() => {
  gameLoop.start()
  bgMusic.play().catch(() => {})

  // Fetch events that occurred while in background
  syncMissedEvents()
})

Unsubscribe

Remove callbacks when they are no longer needed to prevent memory leaks:

js
const unsubBg = await supergame.onBackground(() => { ... })
const unsubFg = await supergame.onForeground(() => { ... })

// On scene change or cleanup:
unsubBg()
unsubFg()

Checklist

Before publishing, verify:

  • [ ] Game HUD does not sit behind the notch / Dynamic Island
  • [ ] body has a full-screen background color — no --sa-top/--sa-bottom padding on body
  • [ ] onBackground stops the game loop
  • [ ] onForeground restarts music
  • [ ] Multiplayer connection stays open in background (platform guarantees this)
  • [ ] Unsubscribe functions are called during scene cleanup