Observed since 20 Oct, 21:35 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
9am-ui
The shared design language, NUI plumbing and FiveM CEF workarounds behind every 9AM Studios script — an open shadcn registry.
The shared design language, NUI plumbing and FiveM CEF workarounds behind every 9AM Studios script.
Distributed as a shadcn registry: components are copied into each script rather than imported from a package, so a script can diverge when it genuinely needs to — while 9am-ui check makes sure nobody diverges by accident.
Extracted from 9am-vehicleshop v1.2.8, which is consumer #0.
| Layer | Items | What it gives you |
|---|---|---|
| Theme | @9am/theme | OKLCH tokens for both palettes, the primary-50…950 ramp, radius scale, class-driven dark: variant, and the NUI base rules (transparent body, suppressed focus rings, scrollbar utilities). Locked — see Drift policy. |
| Fonts | @9am/fonts | Poppins + Phudu embedded as base64. Kept out of @9am/theme on purpose — see Fonts. Locked. |
| Primitives | 19 items | button (8 variants), card, input, label, field, select, multi-select, switch, separator, table, data-table, tabs, dialog, popover, tooltip, info-tip, calendar, chart, viewport |
| Icons | 47 items | @9am/icon-* — hand-built animated SVGs, no icon library dependency. Each exposes startAnimation()/stopAnimation() through a ref so a parent (a tab, a table row) can drive it. |
| NUI | @9am/nui, @9am/visibility, @9am/use-theme, @9am/i18n | fetchNui, useNuiEvent, debugData, browser-dev detection, frame visibility with ESC + focus release, theme store, and runtime i18n pulled from Lua. |
| Lib | @9am/utils, @9am/smooth-scroll | cn(), and the global wheel handler that repairs scrolling in CEF. |
| Scaffold | @9am/scaffold + 9am-ui lua | A whole new script's UI: vite/tsconfig/eslint/postcss, index.html, main.tsx, starter App, plus the Lua side. |
Browse it all: bun run preview → http://localhost:5173.
@9am/viewport is the house scroll container: a drag-able custom scrollbar, layered "liquid glass" edge blur, and a header that blur-swaps to a sticky copy as you scroll. It also carries the CEF-103 workarounds — clipPath instead of overflow+radius, stacked fixed-blur bands because mask doesn't apply to backdrop-filter output, and JS-computed grid columns because :has() and @container don't exist in Chromium 103.
@9am/smooth-scroll exists because CEF will not wheel-scroll a clip-path'd container at all. Without it, the viewport is unscrollable in-game while working fine in your browser.
mkdir -p my-script/web && cd my-script/web
bun init -y
Add components.json:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "rsc": false, "tsx": true,
"tailwind": {
"config": "", "css": "src/index.css",
"baseColor": "neutral", "cssVariables": true, "prefix": ""
},
"aliases": {
"components": "@/components", "utils": "@/lib/utils",
"ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks"
},
"registries": {
"@9am": {
"url": "https://raw.githubusercontent.com/ilovehugetits/9am-ui/main/r/{name}.json"
}
}
}
Then:
bunx shadcn@latest add @9am/scaffold # web skeleton + every dependency
cd .. && bunx 9am-ui lua # Lua side, from the resource root
The CLI is on npm as
9am-ui. Examples usebunxbecause the rest of the kit assumes bun, butnpx 9am-uiworks identically — the published binary is plain ESM with anodeshebang.
Finally, edit web/src/nui.config.ts — it is the one kit file you are meant to change:
export const nuiConfig = {
resourceName: "my-script",
themeStorageKey: "my-script:theme",
defaultTheme: "dark",
}
Keep the localStorage key in index.html in sync with themeStorageKey (it runs before first paint to avoid a theme flash), then:
cd web && bun install && bun run build
bunx 9am-ui doctor # confirms the registry, theme import and the CEF css setup
Add the registries block above to your components.json, then take what you want:
bunx shadcn@latest add @9am/theme @9am/viewport @9am/data-table
Transitive dependencies resolve automatically — @9am/data-table pulls button, input, table, i18n and its three icons.
After adding @9am/theme, import it from your entry stylesheet:
@import "tailwindcss";
@import "tw-animate-css";
@import "./styles/9am-fonts.css"; /* or 9am-fonts-linked.css — see below */
@import "./styles/9am-theme.css";
9am-theme.css deliberately does not import a font stylesheet, because which one you want depends on your app:
| Use when | How | |
|---|---|---|
@9am/fonts (base64) | Single html entry — the common case | bunx shadcn@latest add @9am/fonts |
| Linked woff2 | Several html entries, especially DUI texture pages | bunx 9am-ui fonts, then import 9am-fonts-linked.css |
Embedding is the better default: one command, nothing else to fetch, and in a NUI everything is served off local disk so there is no network round-trip to save. It becomes the wrong choice as soon as 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.
9am-vehicleshop is the worked example: it has three entries (main NUI, phone, showroom stand) and client/showroom.lua calls CreateDui once per showroom slot, so it uses the linked variant everywhere.
FiveM's CEF is Chromium 103. Two things will silently ruin a build:
postcss.config.js must keep postcss-color-converter. The tokens are authored in oklch(), which CEF cannot parse. Without the conversion the UI renders completely unstyled in-game while looking perfect in bun run start. Verify with:
bun run build && grep -c oklch dist/assets/*.css # must be 0
installSmoothScroll() must run in main.tsx. Otherwise nothing scrolls in-game.bunx 9am-ui doctor checks both, including scanning your built CSS for surviving oklch().
bunx 9am-ui check # run from web/, wire it into CI
| What changed | Result |
|---|---|
src/styles/9am-theme.css or 9am-fonts.css | Fails (exit 1). Identical tokens are what make the scripts read as siblings. Restore with bunx shadcn@latest add @9am/theme @9am/fonts --overwrite, or make the change here and release it. |
| Any component or icon | Warns (exit 0). Allowed — teams need the escape hatch — but a local fix helps nobody else. Consider upstreaming. |
src/nui.config.ts | Ignored. It is supposed to differ. |
Anything from @9am/scaffold | Ignored. A starting point, not a managed artifact. |
bun run build # regenerates fonts, registry.json and r/
bun run typecheck
bun run preview # eyeball it in both themes
git commit -am "feat(button): ..." && git push
r/ is committed on purpose — raw.githubusercontent.com serves committed files, which is what lets this repo act as a registry with no hosting at all.
Consumers pick the change up on their next shadcn add ... --overwrite. Nothing auto-updates; that is deliberate.
See CONTRIBUTING.md for adding a component.
MIT. Copy the components into your own scripts freely — that is what a copy-in registry is for.
Poppins and Phudu ship under the SIL Open Font License 1.1, which travels with them.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
clsxEnvironment{
"local_name": "clsx",
"alias": false,
"peer_metadata": null
} | ^2.1.1 | runtime |
reactEnvironment{
"local_name": "react",
"alias": false,
"peer_metadata": null
} | 19.2.8 | runtime |
motionEnvironment{
"local_name": "motion",
"alias": false,
"peer_metadata": null
} | ^12.42.2 | runtime |
zustandEnvironment{
"local_name": "zustand",
"alias": false,
"peer_metadata": null
} | ^5.0.14 | runtime |
rechartsEnvironment{
"local_name": "recharts",
"alias": false,
"peer_metadata": null
} | ^3.10.0 | runtime |
react-domEnvironment{
"local_name": "react-dom",
"alias": false,
"peer_metadata": null
} | 19.2.8 | runtime |
tailwindcssEnvironment{
"local_name": "tailwindcss",
"alias": false,
"peer_metadata": null
} | ^4.3.3 | runtime |
tailwind-mergeEnvironment{
"local_name": "tailwind-merge",
"alias": false,
"peer_metadata": null
} | ^3.6.0 | runtime |
tw-animate-cssEnvironment{
"local_name": "tw-animate-css",
"alias": false,
"peer_metadata": null
} | ^1.4.0 | runtime |
react-day-pickerEnvironment{
"local_name": "react-day-picker",
"alias": false,
"peer_metadata": null
} | ^10.0.1 | runtime |
@tailwindcss/viteEnvironment{
"local_name": "@tailwindcss/vite",
"alias": false,
"peer_metadata": null
} | ^4.3.3 | runtime |
@radix-ui/react-slotEnvironment{
"local_name": "@radix-ui/react-slot",
"alias": false,
"peer_metadata": null
} | ^1.3.1 | runtime |
@radix-ui/react-tabsEnvironment{
"local_name": "@radix-ui/react-tabs",
"alias": false,
"peer_metadata": null
} | ^1.1.19 | runtime |
@radix-ui/react-labelEnvironment{
"local_name": "@radix-ui/react-label",
"alias": false,
"peer_metadata": null
} | ^2.1.13 | runtime |
@tanstack/react-tableEnvironment{
"local_name": "@tanstack/react-table",
"alias": false,
"peer_metadata": null
} | ^8.21.3 | runtime |
@radix-ui/react-dialogEnvironment{
"local_name": "@radix-ui/react-dialog",
"alias": false,
"peer_metadata": null
} | ^1.1.21 | runtime |
@radix-ui/react-selectEnvironment{
"local_name": "@radix-ui/react-select",
"alias": false,
"peer_metadata": null
} | ^2.3.5 | runtime |
@radix-ui/react-switchEnvironment{
"local_name": "@radix-ui/react-switch",
"alias": false,
"peer_metadata": null
} | ^1.3.5 | runtime |
@radix-ui/react-popoverEnvironment{
"local_name": "@radix-ui/react-popover",
"alias": false,
"peer_metadata": null
} | ^1.1.21 | runtime |
@radix-ui/react-tooltipEnvironment{
"local_name": "@radix-ui/react-tooltip",
"alias": false,
"peer_metadata": null
} | ^1.2.14 | runtime |
class-variance-authorityEnvironment{
"local_name": "class-variance-authority",
"alias": false,
"peer_metadata": null
} | ^0.7.1 | runtime |
viteEnvironment{
"local_name": "vite",
"alias": false,
"peer_metadata": null
} | ^8.1.5 | development |
@types/bunEnvironment{
"local_name": "@types/bun",
"alias": false,
"peer_metadata": null
} | ^1.3.5 | development |
typescriptEnvironment{
"local_name": "typescript",
"alias": false,
"peer_metadata": null
} | ^5.9.3 | development |
@types/reactEnvironment{
"local_name": "@types/react",
"alias": false,
"peer_metadata": null
} | ^19.2.17 | development |
@types/react-domEnvironment{
"local_name": "@types/react-dom",
"alias": false,
"peer_metadata": null
} | ^19.2.3 | development |
@vitejs/plugin-reactEnvironment{
"local_name": "@vitejs/plugin-react",
"alias": false,
"peer_metadata": null
} | ^6.0.4 | development |
Source distribution
{
"shasum": "fb486fbc84c51b79d02466ac7c451df532ea1b2f",
"tarball": "https://registry.npmjs.org/9am-ui/-/9am-ui-1.1.0.tgz",
"fileCount": 201,
"integrity": "sha512-8pg6dLkMOzmFI7juHMWdT0MDnkQQ8Emn+/ifTr8MtXRd+KN1M/ehyWf7Jwbj0OtKXDjkiwHDaOMFakEQjagUAQ==",
"signatures": [
{
"sig": "MEYCIQCc69RXhYamiEjzT8+pffWZ2f/uUhZBQHyeUjUrV00oOQIhAKyrqUZlLnjaGedkI4ZTHkrKRbDFEOaHK5yP/REagLVv",
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
}
],
"attestations": {
"url": "https://registry.npmjs.org/-/npm/v1/attestations/9am-ui@1.1.0",
"provenance": {
"predicateType": "https://slsa.dev/provenance/v1"
}
},
"unpackedSize": 1636127
}Observed since 20 Oct, 21:35 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:207e6b724a570bf0bc0ec5861a53d3cc8b98bc89e296f145ab734d7f74b686e4