Styling
A module renders in its own iframe, so none of the overlay's CSS leaks in. Instead, the host gives you a base stylesheet and a set of theme variables so your module matches the overlay out of the box — and updates live when the user changes theme.
What you get automatically
When a surface mounts, the SDK injects a small base stylesheet so a plain module looks integrated with no effort:
html, body { margin: 0; background: transparent; }
html { overflow: hidden; } /* windows use overflow: auto instead */
* { box-sizing: border-box; }
body {
font-family: var(--ps-font, system-ui, -apple-system, sans-serif);
color: var(--ps-foreground, #e6e6e6);
font-size: 13px;
line-height: 1.4;
}
a { color: var(--ps-accent, #3d9dc0); }
/* themed, thin scrollbars are styled too */
So a module that writes plain text already has the right font, colour, and a transparent background that lets the overlay's material show through.
Theme variables
The host resolves the current theme to these CSS custom properties on your iframe root and re-pushes them whenever the theme or opacity changes — so just use them in your CSS and your module re-themes itself.
| Variable | Example value | Use for |
|---|---|---|
--ps-background | hsl(220 6% 9%) | the app background colour |
--ps-foreground | hsl(0 0% 92%) | primary text |
--ps-panel | hsl(220 6% 13%) | a raised surface / card background |
--ps-panel-2 | hsl(220 6% 17%) | a secondary / nested surface |
--ps-accent | hsl(196 52% 50%) | accent — links, highlights, primary buttons |
--ps-accent-2 | hsl(262 52% 60%) | a secondary accent (gradients, variety) |
--ps-muted | hsl(220 6% 60%) | muted / secondary text |
--ps-overlay-opacity | 0.92 | the painted-surface translucency (a unitless number) |
--ps-font | a font-family stack | the overlay UI font |
Colours arrive as ready-to-use hsl(...) strings — drop them straight into any property:
.card {
background: var(--ps-panel);
color: var(--ps-foreground);
border: 1px solid color-mix(in srgb, var(--ps-foreground) 12%, transparent);
border-radius: 10px;
padding: 10px 12px;
}
.card a, .card .accent { color: var(--ps-accent); }
.card small { color: var(--ps-muted); }
color-scheme
The host also sets color-scheme (light or dark) on your <body>, so native form controls and
scrollbars render in the right scheme. (It's intentionally on <body>, not the root — putting it on the
root would paint a solid canvas and defeat the transparent background.)
Reacting in code
You rarely need to: the SDK applies the variables for you, so CSS that uses var(--ps-*) just works. If
you compute styles in JS (e.g. drawing to a <canvas>), read them from ps.theme and subscribe to changes:
function paint() {
const accent = ps.theme['--ps-accent'] ?? '#3d9dc0'
// ...draw with `accent`...
}
paint()
ps.onThemeChange(paint) // fires on theme / opacity change
Sizing
You don't set a surface's size directly — the host does, based on kind:
- Widgets and settings auto-size to their content. The SDK runs a
ResizeObserveron<body>and reports the content size, so the surface grows/shrinks to fit. Lay out your content naturally; avoid fixed100vh/100vwheights. - Windows fill the host window body, which the user can resize. Use
height: 100%and let your content scroll (windows getoverflow: auto).
Typing and focus
When a text field inside your iframe gains focus (input, textarea, or contenteditable), the SDK tells
the host so the click-through overlay accepts keystrokes — you get working text entry for free, no extra
code.
Tips
- Keep backgrounds transparent or translucent (
var(--ps-panel)over the overlay material reads best); a fully opaque fill looks heavy floating over a game. - Use
color-mix(in srgb, var(--ps-foreground) N%, transparent)for subtle borders/dividers that adapt to the theme instead of hard-coding greys. - Respect the user's setting to keep motion subtle — avoid large, constant animations in a widget that sits over gameplay.