# mri-fivem-liquid-glass (full docs) # For AI agents This project is built to be used and improved by AI coding agents as much as by people. Three steps and your agent knows the whole library: `setup-ai` writes the library skill where each agent looks for it (`.claude/skills/`, `AGENTS.md`, `.cursor/rules/`, `.github/copilot-instructions.md`). Running it again updates the same block instead of duplicating it. Without `--agent` it detects the agents already used in the project. ## Using the library from an agent - **Agent skill**: the npm package ships `skills/mri-fivem-liquid-glass/SKILL.md`, a ready skill in the Claude Code / Agent Skills format. Copy it into your agent's skills folder, or point the agent at `node_modules/mri-fivem-liquid-glass/skills/mri-fivem-liquid-glass/SKILL.md`. - **[`/llms.txt`](/llms.txt)**: a short map of the docs and the API for language models; [`/llms-full.txt`](/llms-full.txt) has every guide page in one file. - **[JSON API](/api)**: options, attributes and presets as data, always generated from the source. ## Improved it? Send it back If your agent fixed a bug, tuned a shader or added a feature while using this library in a project, **open a pull request upstream** instead of keeping a private patch. Everyone gets the improvement and your project keeps getting updates without conflicts. 1. Read [`AGENTS.md`](https://github.com/mur4i/mri-fivem-liquid-glass/blob/main/AGENTS.md) in the repository: how to build, test and what reviewers expect. 2. Keep the change focused and explain what was seen in game versus in the browser. 3. Open the pull request at [github.com/mur4i/mri-fivem-liquid-glass](https://github.com/mur4i/mri-fivem-liquid-glass/pulls). Changes to docs, examples and the community registry merge automatically when the checks pass. Changes to the library code get a quick review from a maintainer. ## Ground rules for agents - Target the FiveM browser: Chromium 103 and WebGL2. No newer browser APIs. - Never copy code from closed or paid resources. - Do not claim it was tested in game unless it was. --- # Contributing Thanks for helping. Issues and pull requests in English or Portuguese are both welcome. ## Fastest ways to help - **Share your project** in the [Showcase](/showcase): add one JSON file, it merges by itself. - **Share a preset** in [Presets](/presets): same thing. - **Report numbers**: FPS and `resmon` on your hardware, as an issue. - **Improve the docs**: every page has an "Edit this page" link at the bottom. ## Registry pull requests Add one file per entry. The CI validates it against the schema and merges it when green. ::: code-group ```json [registry/showcase/my-server.json] { "id": "my-server", "name": "My Server HUD", "author": "your-github-user", "description": "Inventory and phone with liquid glass.", "url": "https://example.com", "repo": "https://github.com/you/your-resource", "image": "my-server.webp", "tags": ["hud", "inventory"] } ``` ```json [registry/presets/my-preset.json] { "id": "my-preset", "name": "My preset", "author": "your-github-user", "description": "What it looks like and where it fits.", "options": { "blur": 16 }, "attributes": { "data-glass": "liquid", "data-glass-bezel": "20" } } ``` ::: Showcase images go in `registry/showcase/images/`, WebP, at most 300 KB. Not comfortable with pull requests? Use the issue forms ["Add my project to the showcase"](https://github.com/mur4i/mri-fivem-liquid-glass/issues/new?template=showcase.yml) and ["Share a preset"](https://github.com/mur4i/mri-fivem-liquid-glass/issues/new?template=preset.yml). ## Code Read [CONTRIBUTING.md](https://github.com/mur4i/mri-fivem-liquid-glass/blob/main/CONTRIBUTING.md) for the development setup, code rules and commit format. --- # FiveM resource Patterns that matter once the glass lives in a real resource. The full working version is [`examples/showcase-resource`](https://github.com/mur4i/mri-fivem-liquid-glass/tree/main/examples/showcase-resource) (`/liquidglass` in game). ## Manifest ```lua fx_version 'cerulean' game 'gta5' ui_page 'html/index.html' files { 'html/index.html', 'html/mri-liquid-glass.iife.min.js', -- only if you ship the file instead of the CDN } ``` ## Start once, render on demand Start the glass when the page loads. Open and close screens by adding and removing their markup: with no `[data-glass]` left in the DOM, the render loop stops by itself. Hiding with `display: none` also stops drawing, but keeps a cheap check running every frame. ```js MriLiquidGlass.startGameGlass() window.addEventListener('message', (e) => { if (e.data.action === 'open') root.innerHTML = template() if (e.data.action === 'close') root.innerHTML = '' }) ``` ## Focus ```lua SetNuiFocus(true, true) -- cursor and keyboard go to the page SetNuiFocus(false, false) -- back to the game ``` ## Looking around while a screen is open A nice touch: drag on empty space to turn the camera, release to get the cursor back. Do not drop the focus on mouse down: the game never sees that press and the drag ends immediately. Keep the focus and pass the input through instead, so the page still receives the release: ```lua local blocked = { 24, 25, 140, 141, 142, 257, 263 } -- attack and melee while dragging RegisterNUICallback('lookStart', function(_, cb) cb('ok') looking = true SetNuiFocusKeepInput(true) CreateThread(function() while looking do for i = 1, #blocked do DisableControlAction(0, blocked[i], true) end Wait(0) end end) end) RegisterNUICallback('lookEnd', function(_, cb) looking = false SetNuiFocusKeepInput(false) cb('ok') end) ``` ```js root.addEventListener('pointerdown', (e) => { if (e.target.closest('[data-glass]')) return // buttons and panels keep working post('lookStart') }) window.addEventListener('pointerup', () => post('lookEnd')) window.addEventListener('blur', () => post('lookEnd')) ``` ## Several resources with glass Each NUI page runs its own glass. Two pages open at the same time cost twice; keep glass on the screens the player looks at, not on always-on HUD pieces. ## DUI and iframes DUI pages (`?mode=dui`) are skipped on purpose. Pages inside iframes are not tested yet: if you try, tell us in an issue. --- # React, Vue, Svelte The library is framework free: start it once, mark elements with attributes. These snippets show where "once" lives in each framework. ::: code-group ```tsx [React] import { useEffect } from 'react' import { startGameGlass } from 'mri-fivem-liquid-glass' export function App() { useEffect(() => { const glass = startGameGlass({ fallbackImage: import.meta.env.DEV ? '/dev-bg.jpg' : null }) return () => glass.stop() }, []) return
Hello
} ``` ```vue [Vue] ``` ```svelte [Svelte]
Hello
``` ```html [Plain HTML]
Hello
``` ::: ## TypeScript and `data-*` in JSX `data-glass` and friends are plain attributes, so no typing is needed. The option types ship with the package: `GameGlassOptions`, `GameGlassHandle`. ## MRI ui-kit Projects on [`@mriqbox/ui-kit`](https://github.com/mri-Qbox-Brasil/mri-ui-kit) 4.34+ already get this library through the kit: `startGameGlass` is re-exported, and `startSuiteGlass` turns the suite's "liquid" theme into glass on every surface. --- # Quick start ## 1. Install ::: code-group ```bash [npm] npm i mri-fivem-liquid-glass ``` ```bash [pnpm] pnpm add mri-fivem-liquid-glass ``` ```html [No bundler] ``` ::: Offline or pinned: copy `node_modules/mri-fivem-liquid-glass/dist/mri-liquid-glass.iife.min.js` next to your `index.html` and list it in the `files` of your `fxmanifest.lua`. ## 2. Start it once ```ts import { startGameGlass } from 'mri-fivem-liquid-glass' startGameGlass() ``` With the script tag: `MriLiquidGlass.startGameGlass()`. It returns `{ refresh, stop }`. Call it once when the NUI boots, not every time a screen opens. ## 3. Mark your surfaces ```html
Frosted panel
``` ## 4. Three CSS rules ```css /* The page must stay transparent so the game shows through. */ html, body, #root { background: transparent; } /* The library draws on a canvas at z-index 0; your UI goes above it. */ #root { position: relative; z-index: 1; } [data-glass] { background: rgba(255, 255, 255, 0.06); /* tint over the blur: keep it low */ border: 1px solid rgba(255, 255, 255, 0.22); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 16px 36px rgba(0, 0, 0, 0.3); } /* Flip the text over bright scenes. */ [data-glass-backdrop='bright'] { color: #111; } ``` ::: danger Do not use backdrop-filter Remove every `backdrop-filter` from glass elements. In CEF it composites against the page, not the game, and paints a solid layer over the real glass. ::: ## 5. Develop outside the game In a normal browser there is no game frame, so the library stays off. Pass a screenshot to see the real look while you design: ```ts startGameGlass({ fallbackImage: import.meta.env.DEV ? '/dev-bg.jpg' : null }) ``` `fallbackImage` takes a URL, an `` or a ``. ## Next - Tune the look in the [Playground](/playground) and copy the code. - Read the [FiveM resource](./fivem) patterns (focus, camera drag, closing screens). --- # How it works ## 1. Getting the game into the page The FiveM browser (CEF, Chromium 103) draws the NUI page on top of the game. The page itself is transparent and never contains the game pixels, which is why `backdrop-filter` blurs nothing. The Cfx.re NUI layer has a hook for this. When a page creates a WebGL texture with a 1x1 pixel and then sets `TEXTURE_WRAP_T` in this exact order: ```js gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT) gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT) gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) ``` the GPU process binds the game back buffer to that texture. From then on, sampling it returns the current game frame (without the NUI, so there is no feedback loop). The FiveM main menu and [screenshot-basic](https://github.com/citizenfx/screenshot-basic) use the same mechanism. Until the hook binds, the texture is still the placeholder pixel or comes out black. The game can also recreate its back buffer, which drops the binding. ## 2. Pipeline per frame ``` game frame ──► half size (sharp) ──► quarter size ──► gaussian passes ──► temporal mix ──► blur │ │ │ └──► 32x18 thumbnail ──► async readback (PBO + fence) └───────────────────────────────────────────────► liquid rim refraction ``` 1. **Downscale with 4 bilinear taps per texel.** Copying straight to a quarter size skips pixels and shimmers when the camera moves. 2. **Short gaussian taps.** A 9 tap kernel folded into 5 linear fetches, with each tap at most about 1.5 texels apart. The pass count comes from the requested sigma (`ceil((sigma / 2.4)^2)`, at most 8). Wide taps turn thin details (poles, fences) into patterns. 3. **Temporal mix.** Each new blur is mixed with the previous one to hide the remaining flicker. `temporal` (default 0.45) is the weight at 60 fps, a time constant of about 28 ms; the real weight is `1 - (1 - temporal)^(dt / 16.7 ms)`, so the smoothing feels the same at 30 or 144 fps. 4. **Composition.** One quad per element, in full screen resolution, so rounded edges stay crisp. A signed distance function of the rounded box gives the antialiased edge; ancestor opacity and the nearest `overflow` ancestor (scissor) are applied. ## 3. Broken frames Reading pixels straight from the GPU stalls until the game frame is done. Instead, a 32x18 thumbnail goes into a pixel buffer and is read one or two frames later, when its fence signals. The blur only advances when that thumbnail is approved. Every limit is in milliseconds, so it behaves the same at any frame rate: - **All black**: keep the last good blur. Black for 2.5 s means the hook dropped: recreate the hook texture (at most every 4 s). - **Sudden darkening**: the share of dark pixels jumps 8 points above its running average, or the brightness falls below 55% of it (scenes already darker than about 10% are left alone). Loading fades and menus do this; up to 800 ms of it is hidden behind the last good frame. The averages follow the scene with a 250 ms half life. - **Fence never signals** for 4 s: rebuild every GL resource. The same thumbnail feeds `data-glass-backdrop`. Gamma luma around 0.46 (linear about 0.18) is where black and white text have the same contrast, so the text turns dark above 0.52 and light again below 0.40; the gap keeps it from flickering. ## 4. Liquid rim The sharp half size frame is double buffered and only swapped in after approval, so a black frame never shows up in the rim. Inside the rim, with `rim` going from 1 at the edge to 0 where the bezel ends: - **Refraction**: sample the sharp frame at `normal * rim^2.2 * refraction` outwards, so the edge shows the backdrop from just outside the element, bent like a convex lens. - **Mix**: `smoothstep(0.15, 0.85, rim)` blends sharp and blurred, so the rim is clear and the middle stays frosted for text. - **Dispersion**: red and blue sample with the offset 30% larger and smaller than green. - **Highlight**: `rim^7` times the light direction; the opposite side gets 35%. The `liquid` preset: bezel 22% of the shortest side (8 to 28 px), refraction 0.8 of the bezel, dispersion 0.5, highlight 0.45. ## 5. Shapes and lenses - `data-glass-shape="diamond"` swaps the rounded box distance function for a rounded rhombus. Everything else (rim, refraction, highlight) follows the new outline. - `data-glass-lens="gem"`: a flat table in the middle (42% of the size) magnifies the sharp frame; around it the crown is split into `facets` flat faces. Each face bends the backdrop along its own direction and gets its own brightness from the light, with bright cut lines between faces and stronger color split. - `data-glass-lens="kaleidoscope"`: the angle around the center is folded into one mirrored wedge (`facets` slices), which reads the sharp backdrop behind the element. Moving the element changes the pattern. ## 6. Cost Everything heavy runs at a quarter (blur) or half (sharp) of the screen size. Composition only shades pixels inside glass elements. With no glass element left in the DOM the loop stops and the canvas is removed from the page (see Troubleshooting, AMD panels); with hidden elements it only measures rectangles. --- # Introduction FiveM draws your NUI page on top of the game with the Chromium Embedded Framework (CEF). The page is transparent, and the game is composited underneath it afterwards. That is why CSS `backdrop-filter: blur()` gives you a flat, solid box: from the page's point of view there is nothing behind your panel to blur. **mri-fivem-liquid-glass** gets the live game frame into the page through the Cfx.re NUI game view hook, blurs it on the GPU and draws it behind every element you mark, in the exact shape of the element. On top of that it can bend the sharp image on a bevelled rim (liquid glass), cut it into facets (gem) or mirror it into slices (kaleidoscope). ![In game: liquid glass, a faceted diamond and a kaleidoscope](/hero.webp) ## What you get - **Frosted glass** behind any element, following `border-radius`, ancestor opacity and `overflow` clipping. - **Liquid glass**: bevelled rim, refraction, color split, specular highlight. - **Shapes and lenses**: diamond outline, faceted gem, kaleidoscope. - **Readable text**: `data-glass-backdrop="bright|dark"` on every element. - **Game proof**: survives loading screens, black frames and lost hooks; never stalls the GPU to read pixels. - **No dependencies**, about 18 KB minified, works with any framework or plain HTML. ## Where to go next - [Quick start](./getting-started) to have glass on screen in a few minutes. - [Playground](/playground) to tune the look and copy the code. - [FiveM resource](./fivem) for focus, camera and lifecycle patterns. - [How it works](./how-it-works) if you want the details. --- # Markup Everything is driven by `data-*` attributes on your elements. Changes are picked up by themselves: add, remove or edit an attribute and the glass follows on the next frame. ## Attributes
AttributeValuesSet byEffect
{{ a.name }} {{ a.values }} {{ a.setBy }} {{ a.description }}
Any fine tuning attribute (or a lens) also turns the rim on for a plain `data-glass` element. ## Frosted or liquid? Use **frosted** (`data-glass`) for large panels with content: lists, forms, chat. A bevel there competes with the text. Use **liquid** (`data-glass="liquid"`) for small, highlighted pieces: buttons, pills, orbs, docks, HUD widgets. ```html
...
``` ## Shape follows your CSS The glass reads the element's box every frame: - `border-radius` (from `border-top-left-radius`) rounds the glass, including `50%` circles and `999px` pills; - the opacity of the element **and all its parents** fades the glass, so CSS fade animations work; - the nearest parent with `overflow` other than `visible` clips it; - `transform: scale()` and `translate()` work; rotations are clipped to the bounding box. If you change the radius through inline `style`, call `refresh()` on the handle. Class and `data-glass*` changes are detected automatically. ## Text color `data-glass-backdrop` is written by the library from the brightness behind each element, with hysteresis so it does not flicker: ```css [data-glass-backdrop='bright'] { color: #111; } [data-glass-backdrop='dark'] { color: #fff; } ``` --- # Options ```ts import { startGameGlass, GLASS_DEFAULTS } from 'mri-fivem-liquid-glass' const glass = startGameGlass({ blur: 18, saturation: 1.2 }) // glass.refresh() re-reads radius, clipping and attributes after inline style changes // glass.stop() removes the canvas and stops everything ``` This table is generated from the library source, so it is always current.
OptionTypeDefaultMeaning
{{ o.name }} {{ o.type }} {{ show(o.default) }} {{ o.description }}
The same data is available as JSON at [`/api/v1/options.json`](/api). ## Changing options at runtime Options are read when the glass starts. To change `blur` or `saturation` live, stop and start again; it is cheap: ```ts let glass = startGameGlass({ blur: 14 }) function setBlur(value: number) { glass.stop() glass = startGameGlass({ blur: value }) } ``` Per element looks (bezel, refraction, dispersion, lenses) are attributes, so they change live without a restart. See [Markup](./markup). --- # Performance ## Measured in game With the full [showcase resource](https://github.com/mur4i/mri-fivem-liquid-glass/tree/main/examples/showcase-resource) open (21 glass elements, including a large kaleidoscope), on an RTX 4060 Laptop and i7-13650HX at 1080p: - **`resmon`: 0.00 ms and 0.00%** for the resource: nothing runs in Lua, the work happens on the GPU inside the NUI. - **No measurable FPS drop**: with the screen closed the scene swings between 89 and 98 fps; with everything open it stays around 90. ![resmon at 0.00 ms with the showcase open](/perf-resmon.webp) Got numbers on other hardware? Share them in an [issue](https://github.com/mur4i/mri-fivem-liquid-glass/issues): they help everyone. ## Why it is cheap - The blur runs on a copy of the game at a quarter of the screen size; the sharp copy for refraction is at half size. - The final drawing only shades the pixels inside your glass elements. - Reading back pixels (to detect black frames and to choose the text tone) uses a 32x18 thumbnail read asynchronously, so the GPU never waits. - No glass element in the DOM: the loop stops and the canvas leaves the page (the WebGL context is only created once the first glass element appears). Hidden elements: nothing is drawn. ## Tips - Prefer a few large glass surfaces over many tiny ones. - Keep glass off always-on HUD pieces; it shines on screens the player opens. - Lower `scale` (for example `0.2`) on weak GPUs; raise `blur` to compensate the softer look. - Remove closed screens from the DOM instead of hiding them. --- # Shapes and lenses ## Diamond ```html
``` ```css .gem { width: 160px; height: 160px; border: 0; box-shadow: none; /* your own tint and border follow the diamond too */ clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); } ``` The element stays a box for layout; the glass is drawn as a diamond inside it. `border-radius` rounds the tips. ## Gem A flat table in the middle magnifies the scene; around it the crown is split into facets, each bending the backdrop its own way, with cut lines and strong color split. ```html
``` Works on any shape, but the diamond outline sells it. Raise `data-glass-dispersion` for more rainbow, `data-glass-refraction` for a harder cut. ## Kaleidoscope The backdrop behind the element mirrored into slices around the center. Moving the element (or the camera) changes the pattern. ```html
``` ## Morphing Width, height and `border-radius` can animate with CSS transitions. During the transition the glass needs to re-read the shape each frame: ```ts function followMorph(el: HTMLElement, glass: { refresh(): void }) { let running = true const tick = () => { if (running) { glass.refresh(); requestAnimationFrame(tick) } } el.addEventListener('transitionend', () => { running = false; glass.refresh() }, { once: true }) requestAnimationFrame(tick) } ``` Try every combination in the [Playground](/playground). --- # Troubleshooting ## The glass is a flat, solid box A `backdrop-filter` is still on the element or on a parent. Remove it. ## Nothing shows, not even the tint - The UI root is under the glass canvas: give it `position: relative; z-index: 1`. - `html`, `body` or the root have a background color: make them transparent. ## It works in game but not in the browser Expected: outside FiveM there is no game frame. Pass `fallbackImage` with a screenshot while you develop. ## Black or frozen glass for the first second The game frame hook binds a moment after the page loads. The library keeps the last good frame and re-binds by itself if the frame stays black for 2.5 s. If it never recovers, check the client log (`FiveM.app/logs/CitizenFX_log_*.log`) for lines with `[liquid-glass]` and open an issue. ## The corners are wrong The radius comes from `border-top-left-radius`. Different radii per corner are not supported yet. If you changed it with inline `style`, call `refresh()`. ## Glass over another panel shows the game, not the panel By design: the glass canvas sits under the whole UI and only knows the game frame. Glass over glass works; glass over an opaque NUI panel shows the game behind both. ## A rotated element looks off Rotation is not supported: the glass uses the element's bounding box. Scale and translate work. ## Rounded panels lose their fill on AMD GPUs (fixed in 1.4.1) Up to 1.4.0 the canvas was inserted at start even with no glass element on screen. A GPU canvas under the page makes Chromium put every panel above it on its own compositor layer, and some AMD drivers drop the fill of rounded layers: the border shows, the background is gone, and a `border-radius` of 0 hides the bug. Since 1.4.1 the canvas only exists while a glass element is in the DOM. Update the package; nothing changes in your markup. ## Still stuck Open an [issue](https://github.com/mur4i/mri-fivem-liquid-glass/issues/new/choose) with a screenshot or a short video, the library version and the log lines.