MineWebUI · Fabric 1.21.1

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.

Flexbox layout engine CSS variables & animations GraalVM JS runtime Native DrawContext rendering
Setup

File Structure

Each screen is a self-contained .html file. CSS lives in <style> tags, JS in <script> tags — one file, no build step.

📁 resources/assets/minewebui/ 📁 screens/ my-screen.html ← bundled with the mod
📁 config/minewebui/ 📁 screens/ my-screen.html ← user-editable, loaded at runtime screens.json ← screen registry huds.json ← HUD registry
💡
Two load paths

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.

Concepts

Interface Types

Two rendering modes. Pick one per file.

🖥
Screen
Full-screen GUI that replaces the game view. Supports keyboard and mouse input. Closes on Escape. Register in screens.json, open from JS or a command.
🎯
HUD
Persistent overlay rendered every frame on top of the game. No input handling (JS events only). Can hide vanilla HUD elements. Register in huds.json.

Registering a Screen

JSON config/minewebui/screens.json
[
  { "id": "my-screen", "file": "my-screen" }
]

Open from JavaScript or with a slash command:

JS
Minecraft.actions.openScreen("my-screen");
// or: /minewebui open my-screen

Registering a HUD

JSON config/minewebui/huds.json
[
  {
    "file": "example-hud",
    "hide": ["health", "food", "armor", "experience", "hotbar", "crosshair", "air"]
  }
]
Valid hide values

health · food · armor · experience · hotbar · crosshair · air · mount-health

Reference

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.

displayblock · flex · none
flex-directionrow · column
justify-contentflex-start · center · flex-end · space-between · space-around
align-itemsstretch · center · flex-start · flex-end
positionstatic · relative · absolute · fixed
top / right / bottom / leftpx · %
width / heightpx · %
min-width / max-widthpx · %
padding / marginshorthand · per-side
bordershorthand · per-side
border-radiuspx
background-colorrgba · #hex · name
backgroundlinear-gradient(...)
box-shadowX Y blur color (no inset)
colorrgba · #hex · name
font-sizepx
font-weightbold
text-alignleft · center · right
opacity0 – 1
overflowhidden · visible
gappx
flex-grow / flex-shrinknumber
z-indexinteger
visibilityhidden · visible
CSS variables--name: value; var(--name)
@keyframes+ animation shorthand
transitionprop duration easing
:hover · :active · :focuspseudo-classes
@mediaqueries

Example — gradient with glow

CSS
.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;
}
JavaScript API

Minecraft Object

The global Minecraft object gives you access to player data, world info, window dimensions, and game actions.

Player

MethodReturnsDescription
Minecraft.player.getName()stringPlayer username
Minecraft.player.getHealth()floatCurrent health (0–20)
Minecraft.player.getMaxHealth()floatMax health points
Minecraft.player.getHunger()intFood level (0–20)
Minecraft.player.getLevel()intXP level
Minecraft.player.getGameMode()string"survival" · "creative" · "adventure" · "spectator"
Minecraft.player.getX() / getY() / getZ()doubleWorld coordinates
Minecraft.player.getYaw() / getPitch()floatCamera rotation
Minecraft.player.getPosition(){x, y, z}Position as object

World & Window

MethodReturnsDescription
Minecraft.world.getName()stringWorld/server name
Minecraft.world.getTime()longGame time in ticks
Minecraft.getWindowWidth()intActual window width in pixels
Minecraft.getWindowHeight()intActual window height in pixels

Actions

JS
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")
JavaScript API

DOM API

A subset of the browser DOM, enough to query, create, and mutate your HTML tree at runtime.

JS Querying
document.getElementById("my-id")
document.querySelector(".class")
document.querySelectorAll("div.active")
document.createElement("div")
document.createTextNode("text")
document.body
JS Element manipulation
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)
JavaScript API

Timers & Animation

Standard timer functions work as expected. Prefer onTick() for game-state polling and requestAnimationFrame() for visual animations.

JS
const id = setTimeout(fn, 1000)
clearTimeout(id)

const interval = setInterval(fn, 500)
clearInterval(interval)

const frame = requestAnimationFrame(fn)
cancelAnimationFrame(frame)
JavaScript API

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.

JS
// 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")
Reference

Lifecycle Functions

Declare these functions at the top level of your <script> block. The runtime calls them automatically at the right moment.

FunctionWhen calledTypical use
onMount()Once, after HTML is parsed and the screen/HUD is readyInitial 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 passAdjusting positions that depend on measured element size
onDestroy()When the screen/HUD is closedCleanup, persisting state
JS
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
}
Example

HUD — Health & Hunger

Two gradient bars that flank the hotbar, updated every tick. Hidden in creative and spectator modes.

HTML example-hud.html
<!-- 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>
Example

Screen — Interactive Dialog

A centred dialog with a text input and confirm button. Demonstrates event listeners, keyboard handling, and centring with getBoundingClientRect().

HTML teleport-screen.html
<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>
Important

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 (or absolute) won't auto-size to their children. Always set explicit width and height.

  • ⚠️

    Flex column heights aren't inferred. A display: flex; flex-direction: column container 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) or el.innerText = value. Both work. Avoid innerHTML for live updates.

  • ⚠️

    right: 50% anchors the right edge, not the left. An element with right: 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 the top/left offset.

  • ⚠️

    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: hidden or display: none from JS instead).

  • ⚠️

    background: linear-gradient(...) goes on background, not background-color. Using background-color with a gradient value won't render anything.

  • ⚠️

    box-shadow does not support inset. Outward glow only. Inner shadows are not rendered.