Theming#

In this tutorial we theme a small settings panel end to end. We start with the built-in dark theme, make our own UI follow the theme through USS variables, register a brand theme of our own, and finish by dropping in the premade Asset Store themes (Pixel, Kawaii, Sketch), so one row of buttons swaps the app between six completely different looks: colors, corners, sprites, and fonts included.

Every bullet point is an actionable step. Everything else is explanation.

Project setup#

  • Create a scene, add an App GameObject with a JSRunner, click Initialize Project (the Nameplates tutorial covers this workflow in detail).
  • Make sure the component library is installed: check ~/package.json for onejs-ui, and run npm install onejs-ui inside ~/ if it is missing.

A settings card#

Everything in onejs-ui is themed through design tokens, so a plain composition of its components is already fully themeable. Replace ~/index.tsx:

import { render, View } from "onejs-react"
import { useState } from "react"
import {
    ThemeProvider, Card, Heading, Text, Checkbox, Switch, Slider, VStack,
} from "onejs-ui"

function Settings() {
    const [autosave, setAutosave] = useState(true)
    const [notify, setNotify] = useState(true)
    const [music, setMusic] = useState(70)

    return (
        <Card style={{ width: 400, padding: 24 }}>
            <Heading level={2}>Settings</Heading>
            <VStack gap={12} style={{ marginTop: 14 }}>
                <Checkbox label="Autosave" value={autosave} onChange={setAutosave} />
                <Switch label="Notifications" value={notify} onChange={setNotify} />
                <VStack gap={4}>
                    <Text size="sm" tone="muted">Music volume</Text>
                    <Slider lowValue={0} highValue={100} value={music} onChange={setMusic} />
                </VStack>
            </VStack>
        </Card>
    )
}

function App() {
    return (
        <View style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
            <Settings />
        </View>
    )
}

render(
    <ThemeProvider theme="dark">
        <App />
    </ThemeProvider>,
    __root,
)

ThemeProvider applies the theme once at the render root: it compiles the theme's tokens into --ojs-* USS variables, and every component resolves its colors, radii, and spacing from those. "dark" and "light" are always registered.

Make your own UI follow the theme#

The same variables are available to your own styles. Classes are the mechanism: inline style= objects cannot read USS variables, so anything themeable goes in a .module.uss. Create ~/Settings.module.uss:

.tokenRow {
    background-color: var(--ojs-surface-raised);
    border-width: 1px;
    border-color: var(--ojs-border);
    border-radius: var(--ojs-radius-md);
    padding: 7px 12px;
}

Then add a row to the card, under the heading:

import styles from "./Settings.module.uss"

<View className={styles.tokenRow} style={{ marginTop: 10 }}>
    <Text size="sm" tone="muted">Custom row styled with theme tokens</Text>
</View>

This row now recolors with every theme change, exactly like the built-in components. The full token list is in the theming reference.

A brand theme#

A theme is a flat object, so a brand theme is a spread over a built-in. Register it by name and it becomes available everywhere a theme name is accepted:

import { registerTheme, darkTheme } from "onejs-ui"

registerTheme("emerald", {
    ...darkTheme,
    bg: "#0a1410",
    surface: "#122019",
    surfaceRaised: "#1a2c23",
    overlayHover: "rgba(52, 211, 153, 0.12)",
    border: "#234234",
    primary: "#34d399",
    primaryHover: "#6ee7b7",
    onPrimary: "#04150e",
    ring: "#34d399",
})

A theme switcher#

useTheme() gives any component inside the provider a setTheme that takes a name. A switcher is a list of names:

import { useTheme, Button, HStack } from "onejs-ui"

const THEMES = ["dark", "light", "emerald"]

function ThemeSwitcher() {
    const { setTheme } = useTheme()
    return (
        <HStack gap={4} style={{ marginTop: 16 }}>
            {THEMES.map((name) => (
                <Button key={name} text={name} size="sm" intent="ghost"
                    onClick={() => setTheme(name)} />
            ))}
        </HStack>
    )
}
  • Add <ThemeSwitcher /> under the <Settings /> card and click through the three names.

Swapping a theme recompiles one variables sheet; there is no React re-render of the controls. The custom token row follows along because it reads the same variables.

Add the premade themes#

So far every theme has been flat colors. The premade Asset Store themes show what the same token system does at full stretch: 9-slice sprite frames, control art, and a font per theme. Each one is a UI Cartridge:

  • Select the JSRunner, open the Cartridges tab, and add Assets/Singtaa/Premade/Themes/Pixel/Pixel.asset, Kawaii/Kawaii.asset, and Sketch/Sketch.asset. Each one extracts to ~/@cartridges/@singtaa/ as you assign it (on OneJS 3.1.3 and older, press E or enter Play mode once).
  • Add one import at the top of ~/index.tsx and extend the switcher:
import "onejs:themes" // registers every extracted cartridge theme

const THEMES = ["dark", "light", "emerald", "pixel", "kawaii", "sketch"]

That is the entire integration: onejs:themes registers every extracted theme at build time, and the switcher built for flat themes now swaps sprite frames and fonts too. (It needs onejs-unity 0.2.19+ with themesPlugin() in the esbuild config, which scaffolded projects include; the Premade Themes guide covers older projects and per-theme explicit imports.)

One practical note for switchers that cross font themes: give the switcher buttons a fixed width and height. A premade font changes text metrics, and fixed boxes keep the row from reflowing mid-click.

Where to go next#

  • The theming reference covers every token, the component skin slots, and applyTheme for advanced setups like theming a secondary panel.
  • The Premade Themes guide covers customizing a premade theme (spread its exported tokens), updating after a package update, and the art and font licensing rules.
  • The Inventory tutorial builds a full game UI on these same variables, including custom slot styling that survives every theme swap.