Skip to main content

How the theme is structured

The theme installs as two stylesheets in src/styles/: Both are locked. 9am-ui check fails the build if either is edited locally — identical tokens across scripts is the whole point. Put your own styles in index.css, below the imports.

Dark mode is class-driven

Without this, every dark: utility keys off the player’s operating system theme, which is neither what you want nor controllable from the UI. Instead the .dark class on <html> drives everything, and useTheme toggles it. Dark is the shipped default. index.html sets the class before first paint so the wrong theme never flashes.

Palettes

Tokens are authored in OKLCH. Dark ships by default; light is a real second palette, not a tint.
Light mode’s --primary is deliberately a deeper amber than dark mode’s bright gold. Bright gold is unreadable as text on white and washes out as a fill. Choosing one deeper gold that works both as bg-primary with dark text and as text-primary on white is why roughly forty text-primary usages needed no per-file changes when light mode was added.

The primary ramp

Eleven fixed gold steps, independent of the active palette:
Use them as normal Tailwind utilities — bg-primary-500, text-primary-200, border-primary-700.
The ramp lives in @theme inline, which means Tailwind inlines the values into utilities instead of emitting CSS custom properties. var(--color-primary-500) resolves to nothing. Always use the generated utility, and spell the class out in full so Tailwind’s scanner can see it — a template literal like `bg-primary-${n}` produces no CSS.

Radius

radius-sm is --radius - 4px, radius-md is - 2px, radius-lg equals it, radius-xl is + 4px. In practice: rounded-2xl for containers and cards, rounded-lg for controls, rounded-xl for tiles and avatars.

Type

Apply Phudu with !font-[phudu]:

Choosing a font stylesheet

9am-theme.css does not import fonts, because which stylesheet you want depends on your app. Embedding is the better default: one command, nothing else to fetch, and in a NUI everything loads off local disk so there is no network round-trip to save. It becomes the wrong choice the moment you have multiple entry points. Every DUI is its own CEF instance, so each one would decode the entire 322 KB payload again with nothing shared between them. The vehicle shop is the worked example — it has three entries and creates one DUI per showroom slot, so it uses the linked variant.

Styling conventions

These are what make a screen read as 9AM rather than generic shadcn.

Translucent fills, not solid greys

This is the raised-control surface — buttons, inputs, avatar tiles, list rows. Hairline borders are border-black/10 dark:border-white/10.

Gold tint for accent states

#F6E3711a is gold at 10% opacity. It is the hover and active wash across links, tabs, and badges.

Press feedback

Every clickable element scales down slightly on press.

Motion

Transitions sit between 150 ms and 300 ms with easeOut. Anything slower feels sluggish at the pace players interact with a NUI.
Never author a colour outside the token set. A raw hex in a component is drift that 9am-ui check cannot detect, because it only compares files against the registry — it cannot tell that your custom blue was never part of the design language.

Base rules the theme applies

The theme sets a few NUI-specific defaults you should know about:
  • html, body, and #root are transparent and overflow: hidden — the NUI is an overlay on the game, not a page.
  • Focus outlines are suppressed globally. A NUI is pointer-driven, and keyboard focus otherwise draws a ring around whatever was last clicked.
  • Images are not draggable and number inputs have no spinner buttons.
  • .scrollbar-none, .scrollbar-mini, and .scrollbar-mini-absolute utilities are available.