Build Minecraft UIs with
HTML, CSS & JavaScript
MineWebUI lets you write Minecraft interfaces the way you build web pages: pure HTML markup, a CSS layout engine, and a GraalVM JavaScript runtime — all rendered natively through Minecraft's DrawContext API.
File Structure
Each screen is a self-contained .html file. CSS lives in <style> tags, JS in <script> tags — one file, no build step.
📁 config/minewebui/ 📁 screens/ my-screen.html ← user-editable, loaded at runtime screens.json ← screen registry huds.json ← HUD registry
Files inside resources/ are bundled with the mod jar and load before user files. Files in config/minewebui/screens/ are editable by players at runtime without restarting the game.
Interface Types
Two rendering modes. Pick one per file.
screens.json, open from JS or a command.huds.json.Registering a Screen
[
{ "id": "my-screen", "file": "my-screen" }
]
Open from JavaScript or with a slash command:
Minecraft.actions.openScreen("my-screen");
// or: /minewebui open my-screen
Registering a HUD
[
{
"file": "example-hud",
"hide": ["health", "food", "armor", "experience", "hotbar", "crosshair", "air"]
}
]
hide valueshealth · food · armor · experience · hotbar · crosshair · air · mount-health
CSS Support
A subset of CSS rendered natively — no browser, no WebView. The layout engine implements flexbox, positioning, box model, and a subset of visual properties.
Example — gradient with glow
.health-bar {
background: linear-gradient(to right, #c0392b, #e74c3c);
box-shadow: 0px 0px 8px rgba(231, 76, 60, 0.6);
border-top: 1px solid rgba(255,255,255,0.15);
border-radius: 3px;
height: 8px;
width: 100%;
transition: width 0.2s ease;
}
Minecraft Object
The global Minecraft object gives you access to player data, world info, window dimensions, and game actions.
Player
| Method | Returns | Description |
|---|---|---|
| Minecraft.player.getName() | string | Player username |
| Minecraft.player.getHealth() | float | Current health (0–20) |
| Minecraft.player.getMaxHealth() | float | Max health points |
| Minecraft.player.getHunger() | int | Food level (0–20) |
| Minecraft.player.getLevel() | int | XP level |
| Minecraft.player.getGameMode() | string | "survival" · "creative" · "adventure" · "spectator" |
| Minecraft.player.getX() / getY() / getZ() | double | World coordinates |
| Minecraft.player.getYaw() / getPitch() | float | Camera rotation |
| Minecraft.player.getPosition() | {x, y, z} | Position as object |
World & Window
| Method | Returns | Description |
|---|---|---|
| Minecraft.world.getName() | string | World/server name |
| Minecraft.world.getTime() | long | Game time in ticks |
| Minecraft.getWindowWidth() | int | Actual window width in pixels |
| Minecraft.getWindowHeight() | int | Actual window height in pixels |
Actions
Minecraft.actions.openScreen("file-name") // open another screen
Minecraft.actions.closeScreen() // close current screen
Minecraft.actions.sendChat("hello world") // send chat message
Minecraft.actions.sendCommand("tp 0 64 0") // run a command (no leading /)
Minecraft.actions.showToast("Title", "Subtitle")
Minecraft.actions.copyToClipboard("text")
Minecraft.actions.translate("key.translation") // i18n lookup
// Persistent state — shared across all screens and HUDs
Minecraft.actions.setState("myKey", 42)
const val = Minecraft.actions.getState("myKey")
DOM API
A subset of the browser DOM, enough to query, create, and mutate your HTML tree at runtime.
document.getElementById("my-id")
document.querySelector(".class")
document.querySelectorAll("div.active")
document.createElement("div")
document.createTextNode("text")
document.body
el.getAttribute("name")
el.setAttribute("name", "value")
el.hasAttribute("name")
// Text content — both work
el.innerText = "new text"
el.textContent = "new text"
el.innerHTML = "<span>hi</span>"
// Inline styles
el.style.color = "red"
el.style.display = "none"
el.style.width = "80%"
// Classes
el.classList.add("active")
el.classList.remove("active")
el.classList.toggle("active")
el.classList.contains("active") // → boolean
// Tree
el.children // array of child elements
el.parentNode
el.parentElement
el.appendChild(node)
el.removeChild(node)
el.remove()
// Geometry
el.getBoundingClientRect() // → {x, y, width, height}
// Events
el.addEventListener("click", fn)
el.addEventListener("mouseover", fn)
el.addEventListener("mouseout", fn)
el.addEventListener("keydown", fn) // screens only
el.dispatchEvent(event)
Timers & Animation
Standard timer functions work as expected. Prefer onTick() for game-state polling and requestAnimationFrame() for visual animations.
const id = setTimeout(fn, 1000)
clearTimeout(id)
const interval = setInterval(fn, 500)
clearInterval(interval)
const frame = requestAnimationFrame(fn)
cancelAnimationFrame(frame)
State & Console
The state object is a global key-value store shared across all screens and HUDs in the same session. Use it to pass data between interfaces without Minecraft.actions.setState.
// Write
state.myKey = "value"
state.count = 0
// Read
const val = state.myKey
// Console (visible in Minecraft log / F3 overlay)
console.log("debug info")
console.warn("something looks off")
console.error("something broke")
Lifecycle Functions
Declare these functions at the top level of your <script> block. The runtime calls them automatically at the right moment.
| Function | When called | Typical use |
|---|---|---|
| onMount() | Once, after HTML is parsed and the screen/HUD is ready | Initial setup, caching element refs, first data load |
| onTick() | ~20× per second (every game tick) | Polling player data, updating health bars, live counters |
| onUpdate() | After every re-layout pass | Adjusting positions that depend on measured element size |
| onDestroy() | When the screen/HUD is closed | Cleanup, persisting state |
function onMount() {
// runs once — cache your element refs here
healthBar = document.getElementById("health-bar")
}
function onTick() {
// runs ~20x/sec — great for live data
const hp = Minecraft.player.getHealth()
const pct = (hp / Minecraft.player.getMaxHealth()) * 100
healthBar.style.width = pct + "%"
}
function onUpdate() {
// runs after layout — measure if you need exact positions
}
function onDestroy() {
// cleanup or save state before the screen closes
}
HUD — Health & Hunger
Two gradient bars that flank the hotbar, updated every tick. Hidden in creative and spectator modes.
<!-- Health bar — sits left of centre -->
<div id="health-panel">
<div id="health-label">♥ <span id="hp-num">20</span></div>
<div id="health-track">
<div id="health-bar"></div>
</div>
</div>
<!-- Hunger bar — sits right of centre -->
<div id="hunger-panel">
<div id="hunger-label"><span id="food-num">20</span> 🍗</div>
<div id="hunger-track">
<div id="hunger-bar"></div>
</div>
</div>
<style>
:root {
--bar-h: 8px;
--panel-w: 90px;
--bottom: 26px;
}
#health-panel {
position: fixed;
right: 50%;
bottom: var(--bottom);
width: var(--panel-w);
height: 20px;
margin-right: 4px;
display: flex;
flex-direction: column;
gap: 3px;
align-items: flex-end;
}
#hunger-panel {
position: fixed;
left: 50%;
bottom: var(--bottom);
width: var(--panel-w);
height: 20px;
margin-left: 4px;
display: flex;
flex-direction: column;
gap: 3px;
}
#health-label, #hunger-label {
font-size: 9px;
color: rgba(255, 255, 255, 0.7);
height: 10px;
}
#health-track, #hunger-track {
width: var(--panel-w);
height: var(--bar-h);
background: rgba(0, 0, 0, 0.4);
border-radius: 3px;
border-top: 1px solid rgba(255,255,255,0.08);
border-bottom: 1px solid rgba(0,0,0,0.4);
overflow: hidden;
}
#health-bar {
height: 100%;
background: linear-gradient(to right, #c0392b, #e74c3c);
box-shadow: 0px 0px 6px rgba(231, 76, 60, 0.6);
border-radius: 3px;
transition: width 0.1s ease;
}
#hunger-bar {
height: 100%;
background: linear-gradient(to right, #d4811a, #f39c12);
box-shadow: 0px 0px 6px rgba(243, 156, 18, 0.5);
border-radius: 3px;
transition: width 0.1s ease;
}
</style>
<script>
let healthPanel, hungerPanel, healthBar, hungerBar, hpNum, foodNum
function onMount() {
healthPanel = document.getElementById("health-panel")
hungerPanel = document.getElementById("hunger-panel")
healthBar = document.getElementById("health-bar")
hungerBar = document.getElementById("hunger-bar")
hpNum = document.getElementById("hp-num")
foodNum = document.getElementById("food-num")
}
function onTick() {
const mode = Minecraft.player.getGameMode()
const hidden = mode === "creative" || mode === "spectator"
healthPanel.style.display = hidden ? "none" : "flex"
hungerPanel.style.display = hidden ? "none" : "flex"
if (hidden) return
const hp = Minecraft.player.getHealth()
const maxHp = Minecraft.player.getMaxHealth()
const hunger = Minecraft.player.getHunger()
const hpPct = Math.max(0, Math.min(100, (hp / maxHp) * 100))
const fPct = Math.max(0, Math.min(100, (hunger / 20) * 100))
healthBar.style.width = hpPct + "%"
hungerBar.style.width = fPct + "%"
hpNum.setAttribute("innertext", Math.ceil(hp))
foodNum.setAttribute("innertext", hunger)
}
</script>
Screen — Interactive Dialog
A centred dialog with a text input and confirm button. Demonstrates event listeners, keyboard handling, and centring with getBoundingClientRect().
<div id="dialog">
<div id="title">Teleport to coordinates</div>
<input id="coords-input" type="text" placeholder="x y z">
<div id="actions">
<button id="cancel-btn">Cancel</button>
<button id="confirm-btn">Teleport</button>
</div>
</div>
<style>
#dialog {
position: fixed;
top: 50%;
left: 50%;
width: 280px;
background: rgba(15, 20, 30, 0.95);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0px 8px 32px rgba(0,0,0,0.6);
}
#title {
font-size: 14px;
color: rgba(255,255,255,0.9);
font-weight: bold;
}
#coords-input {
width: 100%;
height: 32px;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 4px;
color: rgba(255,255,255,0.9);
font-size: 12px;
padding: 0 10px;
}
#actions {
display: flex;
flex-direction: row;
gap: 8px;
height: 30px;
}
#cancel-btn, #confirm-btn {
flex-grow: 1;
height: 30px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
#cancel-btn {
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.12);
color: rgba(255,255,255,0.6);
}
#confirm-btn {
background: linear-gradient(to bottom, #5c9a2e, #3e6b1f);
border: 1px solid rgba(255,255,255,0.1);
color: white;
}
#confirm-btn:hover {
background: linear-gradient(to bottom, #6db535, #4a7d24);
}
</style>
<script>
function onMount() {
const dialog = document.getElementById("dialog")
const input = document.getElementById("coords-input")
// Centre the dialog using actual measured size
const rect = dialog.getBoundingClientRect()
dialog.style.top = "50%"
dialog.style.left = "50%"
// Offset by half the element's size to truly centre it
dialog.style.marginTop = "-" + (rect.height / 2) + "px"
dialog.style.marginLeft = "-" + (rect.width / 2) + "px"
document.getElementById("cancel-btn").addEventListener("click", function () {
Minecraft.actions.closeScreen()
})
document.getElementById("confirm-btn").addEventListener("click", function () {
confirm()
})
// Confirm on Enter
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") confirm()
})
}
function confirm() {
const val = document.getElementById("coords-input").innerText.trim()
if (val) {
Minecraft.actions.sendCommand("tp " + val)
Minecraft.actions.closeScreen()
}
}
</script>
Gotchas & Caveats
Key differences from the browser. Read this before scratching your head for an hour.
-
⚠️
Fixed elements need explicit size. Elements with
position: fixed(orabsolute) won't auto-size to their children. Always set explicitwidthandheight. -
⚠️
Flex column heights aren't inferred. A
display: flex; flex-direction: columncontainer won't grow to fit its children automatically — set explicit heights all the way up the hierarchy. -
⚠️
Updating text dynamically. Use
el.setAttribute("innertext", value)orel.innerText = value. Both work. Avoid innerHTML for live updates. -
⚠️
right: 50%anchors the right edge, not the left. An element withright: 50%has its right edge at the 50% mark — it sits to the left of centre.left: 50%puts the left edge at 50% — it sits to the right. Pair them for two elements that meet at the centre. -
⚠️
Centring with JS is more reliable. Use
getBoundingClientRect()after mount to measure the element, then subtract half its size from thetop/leftoffset. -
⚠️
Check game mode before drawing combat HUDs.
getGameMode()returns"creative"or"spectator"— hide health/hunger bars for those modes. -
⚠️
Screens close on Escape automatically. HUDs are always visible — there's no show/hide API (use
visibility: hiddenordisplay: nonefrom JS instead). -
⚠️
background: linear-gradient(...)goes onbackground, notbackground-color. Usingbackground-colorwith a gradient value won't render anything. -
⚠️
box-shadowdoes not supportinset. Outward glow only. Inner shadows are not rendered.