Client Library
@webgui/client is the JavaScript library for pages running inside the WebGUI mod. One package covers plain JS, React, Vue and Svelte.
npm install @webgui/clientReact, Vue and Svelte are optional peer dependencies — importing the core pulls in none of them, and installing the package adds nothing to a plain-JS project.
Replaces @webgui/react, @webgui/vue and @webgui/svelte
Those were three parallel implementations of the same logic that had to be updated in lockstep every time the mod gained an event. They still work and now re-export this package, so nothing breaks if you stay — but new features land here only. See Migrating.
How it works
The mod injects window.webgui into every page it opens and fires CustomEvents on window as things change. The library subscribes at module load and exposes the result as stores; nothing needs to be set up and there is no provider to mount.
Outside the mod — a normal browser tab, a dev server, server-side rendering — every store reads null and every action is a no-op, so the same page runs in both places without guards at each call site.
Plain JavaScript
import { clientStore, runCommand, isInMod } from '@webgui/client'
if (isInMod()) {
clientStore.subscribe(() => {
const c = clientStore.get()
document.querySelector('#hp').textContent = `${c.health} / ${c.maxHealth}`
})
}
document.querySelector('#home').onclick = () => runCommand('spawn')Every store has the same three members:
| Member | Purpose |
|---|---|
subscribe(listener) | Registers a no-argument listener. Returns the unsubscribe function. |
get() | Current value, or null before the mod has sent anything. |
getServerSnapshot() | Always null — there is no mod during server rendering. |
React
import { useWebGUIClient, isInMod, isReady } from '@webgui/client/react'
export function PlayerInfo() {
const client = useWebGUIClient()
if (!isInMod()) return <p>Open this page inside Minecraft.</p>
if (!isReady(client)) return <p>Connecting…</p>
return <p>Hello, {client!.username}</p>
}Requires React 18+. Every hook is built on useSyncExternalStore — concurrent-mode safe, no provider.
Vue
<script setup lang="ts">
import { useWebGUIClient, runCommand } from '@webgui/client/vue'
const client = useWebGUIClient()
</script>
<template>
<p v-if="client">Hello, {{ client.username }}</p>
<button @click="runCommand('spawn')">Teleport to spawn</button>
</template>Requires Vue 3. Composables clean up through onScopeDispose, so they are safe in any effect scope.
Svelte
<script lang="ts">
import { webguiClient, runCommand } from '@webgui/client/svelte'
</script>
{#if $webguiClient}
<p>Hello, {$webguiClient.username}</p>
{/if}
<button on:click={() => runCommand('spawn')}>Teleport to spawn</button>Requires Svelte 4+. The exports are ordinary readable stores, so $store syntax works.
State
| Core | React | Vue | Svelte |
|---|---|---|---|
clientStore | useWebGUIClient() | useWebGUIClient() | webguiClient |
entityStore | useWebGUIEntity() | useWebGUIEntity() | webguiEntity |
deathStore | useWebGUIDeath() | useWebGUIDeath() | webguiDeath |
selectorStore(fn, eq?) | useWebGUISelector(fn, eq?) | useWebGUISelector(fn, eq?) | webguiSelector(fn, eq?) |
Client
Pushed at 20 TPS, once per client tick:
interface WebGUIClient {
playerUuid: string
username: string
webviewMode: 'GUI_SCREEN' | 'HUD_OVERLAY' | 'NONE'
dimension: string // e.g. "minecraft:overworld"
pos: { x: number; y: number; z: number }
look: { yaw: number; pitch: number } // head rotation, degrees
fov: number // vertical field of view, degrees
lookingAt: LookingAt // what the crosshair is on
health: number
maxHealth: number
food: number // 0–20
xpLevel: number
gamemode?: 'survival' | 'creative' | 'adventure' | 'spectator'
server?: { address?: string; ping?: number }
}Looking at
What the crosshair is on, refreshed with the rest of the client state. This is the game's own answer — the one it uses to draw the block outline and the name above a mob — not a second ray cast with its own idea of reach.
type LookingAt =
| { type: 'none' }
| { type: 'block'; block: string; pos: { x: number; y: number; z: number };
face: 'up' | 'down' | 'north' | 'south' | 'east' | 'west'; distance: number }
| { type: 'entity'; uuid: string; entityType: string; name: string;
pos: { x: number; y: number; z: number }; distance: number }| Field | Notes |
|---|---|
type | Always present. none when the player is looking at nothing, rather than the field disappearing. |
block | Registry id, e.g. minecraft:gold_block. |
pos | Whole block coordinates for a block; the entity's own position for an entity. |
face | Which side of the block the line of sight met. |
distance | Eyes to the point of contact — for a block that is its surface, so it reads about half a block shorter than the distance to its centre. |
Entity
Set when the GUI was opened by right-clicking a bound entity (see /webgui bind); null when it was opened by command.
interface WebGUIEntity {
uuid: string
type: string // e.g. "minecraft:villager"
name: string // custom name, else the type's display name
pos: { x: number; y: number; z: number }
}Death
Only ever set on a page the server configured as the death screen. See webgui:death for the field reference.
import { useWebGUIDeath, useRespawn } from '@webgui/client/react'
export function DeathScreen() {
const death = useWebGUIDeath()
const respawn = useRespawn()
if (!death) return null
return (
<div>
<h1>You died</h1>
<p>{death.deathMessage}</p>
{death.canRespawn && <button onClick={respawn}>Respawn</button>}
</div>
)
}Selectors
The mod pushes client updates 20 times a second, so a component reading only health would otherwise re-render every time the player walks. A selector narrows that:
const health = useWebGUISelector((c) => c.health)Pass an equality function when the selector builds a fresh object:
const pos = useWebGUISelector(
(c) => ({ x: c.pos.x, z: c.pos.z }),
(a, b) => a.x === b.x && a.z === b.z,
)Actions
postToGame, closeGui, runCommand and respawn are plain functions, exported from every entry point.
postToGame({ channel: 'shop:buy', item: 'diamond' }) // → the server, as a page event
closeGui() // closes the GUI or HUD
runCommand('give @s minecraft:diamond 1') // runs as the player
respawn() // only on a custom death screenReact also exports usePostToGame, useCloseGui, useRunCommand and useRespawn, which return the same functions with stable identity for dependency arrays.
runCommand needs the server's permission
Commands are accepted only from the main frame of an origin listed in trustedCommandOrigins. From anywhere else the mod drops the request silently. They run with the player's own permissions, so there is no privilege escalation.
Rate limit
The server caps page events per player per second (pageEventsPerSecond, default 20). postToGame, runCommand, closeGui and respawn all count toward it. Do not call them in a render loop.
Helpers
| Core | React | Vue | Svelte |
|---|---|---|---|
isInMod() | same | same | same |
isReady(value) | same | same | same |
getToken(param?) | useWebGUIToken(param?) | useWebGUIToken(param?) | webguiToken(param?) |
onWebGUIEvent(name, fn) | useWebGUIEvent(name, fn) | useWebGUIEvent(name, fn) | onWebGUIEvent(name, fn) |
getToken returns the signed token the mod appended to the page URL, for backend verification. onWebGUIEvent subscribes to a named event the server sends with emitToPage; it returns an unsubscribe function, while the framework versions clean up on their own.
What the browser does not do
The page runs in Chromium, but it is a page inside a game, not a browser tab:
window.open | Does nothing. There is no window to open one in; open pages through the mod instead. |
| Downloads | Work — the file lands in webgui-downloads/ under the game directory and the player is told the name. |
window.webgui.assetsBase | Set when the server hosts the page's files; see Pages the server hosts. |
window.webgui.isHud | true when the page is the transparent HUD overlay rather than a full-screen GUI. Useful for dropping a background and for skipping input handling the HUD never receives. |
| Right-click menus, printing, DevTools shortcuts | Not available. For errors, see Debugging a page. |
Events that fire before your code runs
The mod sends webgui:death exactly once, right after the document loads, and sets window.webgui.client, .entity and .death before dispatching. Every store reads those snapshots at import time, so a component that mounts a tick late still sees the value.
This is the reason to use the library's stores rather than a bare addEventListener in a component — that listener attaches after the event has already fired and never hears anything.
Migrating
Names are unchanged, so the import path is usually the only edit:
- import { useWebGUIClient } from '@webgui/react'
+ import { useWebGUIClient } from '@webgui/client/react'- import { useWebGUIClient } from '@webgui/vue'
+ import { useWebGUIClient } from '@webgui/client/vue'- import { webguiClient } from '@webgui/svelte'
+ import { webguiClient } from '@webgui/client/svelte'Mixing the two during a gradual migration is safe: the old packages re-export this one, so both import paths reach the same store rather than two copies with different values.