Skip to main content

The two directions

Install the whole layer with bunx shadcn@latest add @9am/nui @9am/visibility @9am/use-theme @9am/i18n.

fetchNui

Three behaviours worth knowing:
  • Ten-second timeout. A Lua callback that never calls cb() would otherwise hang the promise forever.
  • Browser dev mode. When running under bun run start and mockData is supplied, it returns the mock without touching the network.
  • It never rejects. On failure it returns mockData if that was an array, otherwise { success: false, message: "Request failed" }. Awaiting call sites cannot hang or throw an uncaught rejection.
The resource name comes from window.GetParentResourceName() in CEF, falling back to nuiConfig.resourceName in the browser.
Every RegisterNUICallback must call cb(...) exactly once. Returning without calling it leaves the awaiting promise unresolved until the timeout fires.

useNuiEvent

The handler is kept in a ref, so you can pass an inline closure without re-subscribing on every render. On the Lua side:

Visibility

VisibilityProvider shows and hides the app, and guarantees the player never loses input.
It listens for the setVisible action, handles Escape by calling the hideFrame callback, and fades the tree with an opacity and scale transition. Read the state anywhere with useVisibility(). From Lua:
The scaffold’s client/nui.lua also releases focus on onResourceStop. Without that, restarting the resource while the UI is open leaves the player stuck with no input — which happens constantly during development.
Pair it with the error boundary in main.tsx, which calls hideFrame if a render crashes. A UI error must never trap the player.

Theme

A zustand store that toggles the .dark class on <html> and persists to localStorage under nuiConfig.themeStorageKey. index.html reads the same key in an inline script before first paint, so the correct theme is applied before React mounts. Keep the two in sync when you change the key.

Configuration

src/nui.config.ts is the one kit file each script is expected to edit. 9am-ui check skips it deliberately.

Translations

Translations are not bundled. They live in the resource’s locales/*.json and are pulled from Lua at boot, so a customer can retranslate a script without a web rebuild.
1

Boot the dictionary

The optional loader only runs under bun run start, where there is no Lua to ask. The kit does not hardcode the path — only your app knows where its locales live.
2

Use it in components

3

Use it outside components

Placeholders use {name} syntax and are substituted from the vars object.
Never call t at module scope. The dictionary loads asynchronously, so a translated string captured in a top-level constant will be the raw key forever. Store keys in your definition arrays and translate at render time.

Resolution order

  1. The dictionary from Lua
  2. KIT_DEFAULTS — English fallbacks for the kit’s own strings
  3. The key itself
Step two means a freshly scaffolded script renders “No results.” rather than the raw string ui.common.noResults. Your own keys still belong in locales/. Keys the kit uses itself, all under ui.common: close, searchPlaceholder, noResults, noResultsFound, selectPlaceholder, nSelected, rowCount, pageOf.

The Lua side

shared/locale.lua loads English first, deep-merges the active language over it, and exposes both a Lua helper and the NUI callback:
A key missing from the active language falls back to English; missing from both, it returns the key, so a typo is visible in-game rather than a crash. Load it after shared/config.lua — it reads Config.Locale.

Browser development

debugData only fires in development mode and outside CEF, so it is safe to leave in place. Gate dev-only code behind __DEV_SEED__ — the compile-time flag is false for bun run production-build, which removes those branches and their chunks from the customer build entirely.

Smooth scroll

Call this once. It is not optional: CEF will not wheel-scroll a clip-path’d container, which includes every Viewport. It also gives every scroller the same eased feel in the browser and in-game.