An HTML, CSS and JavaScript engine written in Luau, for Roblox.
Not a wrapper around Roblox's UI system — a real rendering pipeline. HTML is tokenized and parsed into a DOM, CSS cascades onto it, a layout engine computes geometry, and a painter emits Roblox Instances. JavaScript runs in an interpreter written from scratch, with the DOM bound to it, so a page can drive its own interface.
local HTMLuau = require(game.ReplicatedStorage.HTMLuau)
local app = HTMLuau.app([==[
<style>
.card { width: 260px; padding: 16px; border-radius: 10px;
background: #fff; border: 1px solid #d8dee9; }
.card h2 { margin: 0 0 8px 0; font-size: 17px; }
.row { display: flex; gap: 8px; }
.row button { flex: 1; }
</style>
<div class="card">
<h2>Hello</h2>
<p id="status">Not clicked yet.</p>
<div class="row">
<button id="go">Click me</button>
</div>
</div>
<script>
let count = 0;
document.getElementById('go').addEventListener('click', () => {
count++;
document.getElementById('status').textContent = `Clicked ${count} times`;
});
</script>
]==], { parent = playerGui.ScreenGui })That renders, lays out, paints, and responds to clicks — with no Luau in the page.
557 tests, run headlessly against the upstream Luau interpreter with a Roblox API shim. Every feature listed in docs/SUPPORTED.md is covered by them.
📖 Read the wiki — installation, a first
page, the DOM API, calling Luau from a page, networking, porting an existing CodePen, and
the limits. Source lives in wiki/; ./tools/publish-wiki.sh pushes it.
git clone https://github.com/Alexpoopy1/HTMLuau
cd HTMLuau
rojo serve
Syncs to ReplicatedStorage.HTMLuau, with the examples alongside it.
python3 tools/bundle.py --mode library --out dist/HTMLuau.luau
Produces one self-contained file. Paste it into a ModuleScript named HTMLuau in
ReplicatedStorage and require it. ./tools/check-bundle.sh verifies that build
actually runs, so it is never shipped untested.
HTML — spec-shaped tokenizer with RAWTEXT and RCDATA handling, a tree builder that
repairs malformed markup (implied end tags, void elements, synthesised html/head/
body), and ~250 character references.
DOM — the node tree, querySelector with a full selector engine, classList,
dataset, innerHTML, and events with real capture/target/bubble dispatch.
CSS — cascade with correct origin and !important precedence, inheritance, 148 named
colours, media queries, and the shorthand set. element.style reads and writes through to
the live attribute.
Layout — block with margin collapsing and the full box model, inline with real line breaking and baseline alignment, flexbox with both axes, and absolute/relative positioning.
Rendering — Roblox Instances with pooling, so a repaint mutates rather than rebuilds.
Borders, radii, gradients, opacity and scrolling map onto UIStroke, UICorner,
UIGradient, CanvasGroup and ScrollingFrame.
JavaScript — lexer, parser and tree-walking interpreter. Closures, classes,
destructuring, template literals, optional chaining, try/catch, Promises, a regex
engine, and the standard built-ins. Bound to the DOM with stable wrapper identity, plus
setTimeout/setInterval/requestAnimationFrame on a proper event loop.
Networking — HttpService with redirects, retries and caching, and a client bridge
guarded by an allowlist, per-player rate limits and size caps. fetch and
XMLHttpRequest for page scripts.
The recommended path. Register capabilities; the page calls them.
local app = HTMLuau.app(html, { parent = screenGui })
app.runtime:register("shop", {
balance = function()
return leaderstats.Coins.Value
end,
buy = function(itemId)
return purchase(player, itemId) -- validate here, as with any player input
end,
})document.getElementById('buy').addEventListener('click', () => {
if (shop.balance() >= 100) shop.buy('sword');
});Nothing arbitrary executes — the page reaches exactly the functions you chose. See docs/SECURITY.md for the other two paths and when they are appropriate.
HttpService is server-only in Roblox, so client code goes through a bridge. Nothing is
permitted until you name it.
-- Server
HTMLuau.serve({
allowlist = HTMLuau.Net.allowlist({ hosts = { "api.yourgame.com" } }),
})-- Client
local browser = HTMLuau.browser({
parent = screenGui,
allowlist = HTMLuau.Net.allowlist({ hosts = { "api.yourgame.com" } }),
})
browser:navigate("https://api.yourgame.com/leaderboard")Private and loopback addresses are refused even if listed. Read docs/SECURITY.md before pointing this at anything.
Register the source locally, or fetch it from an allowlisted CDN:
app.runtime:loadLibrary("mini", require(Libraries.mini)) -- local
app.runtime:libraries():loadFromUrl("https://cdn.example/x.js") -- CDNLibraries that stay within ES5 plus common ES2015 have a real chance of working. Check before you commit to one:
local blockers = HTMLuau.Script.LibraryLoader.probe(source)
-- {} means nothing obvious is in the way; a non-empty list names what is.The usual blockers are generators, Symbol, Proxy, Map/Set, typed arrays and
async/await. This engine is a large documented subset of JavaScript, not V8 —
docs/SUPPORTED.md has the full list.
Stated up front rather than discovered later:
- Remote images cannot render.
ImageLabel.Imageonly accepts Roblox asset URLs, so<img src="https://…">is unrenderable on this platform. Supply anassetMapof URL → asset id for images you have uploaded; anything else gets an alt-text placeholder. - No CSS Grid, floats, or transitions. Flexbox and block layout are solid.
- No
calc()orvar(). A length using either resolves to 0 and collapses the element — substitute literal values. - No
async/awaitsyntax. Promises work; the syntax is not parsed yet. - Strings are byte sequences. Identical to a browser for ASCII; astral-plane characters count as multiple units.
examples/movable-card is the reference: a draggable card written entirely in HTML, CSS
and JavaScript. Dragging it exercises every layer of the engine in one gesture, which is
why the test suite drives it end to end.
local Example = require(ReplicatedStorage.HTMLuauExamples["movable-card"])
Example.mount(playerGui.ScreenGui)./tools/run-tests.sh # full suite
./tools/run-tests.sh css # filter by substring
./tools/check-bundle.sh # verify the drag-in build
python3 tools/gen-docs.py # regenerate the CSS table in SUPPORTED.md
The suite builds the upstream Luau CLI on first run and executes against a Roblox API shim, so no Studio is needed. docs/ARCHITECTURE.md explains how the pipeline fits together and why the significant decisions went the way they did.