Skip to content

Client Library ​

@webgui/client is the JavaScript library for pages running inside the WebGUI mod. One package covers plain JS, React, Vue and Svelte.

bash
npm install @webgui/client

React, 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 ​

js
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:

MemberPurpose
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 ​

tsx
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 ​

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 ​

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 ​

CoreReactVueSvelte
clientStoreuseWebGUIClient()useWebGUIClient()webguiClient
entityStoreuseWebGUIEntity()useWebGUIEntity()webguiEntity
deathStoreuseWebGUIDeath()useWebGUIDeath()webguiDeath
selectorStore(fn, eq?)useWebGUISelector(fn, eq?)useWebGUISelector(fn, eq?)webguiSelector(fn, eq?)

Client ​

Pushed at 20 TPS, once per client tick:

ts
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.

ts
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 }
FieldNotes
typeAlways present. none when the player is looking at nothing, rather than the field disappearing.
blockRegistry id, e.g. minecraft:gold_block.
posWhole block coordinates for a block; the entity's own position for an entity.
faceWhich side of the block the line of sight met.
distanceEyes 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.

ts
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.

tsx
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:

tsx
const health = useWebGUISelector((c) => c.health)

Pass an equality function when the selector builds a fresh object:

tsx
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.

ts
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 screen

React 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 ​

CoreReactVueSvelte
isInMod()samesamesame
isReady(value)samesamesame
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.openDoes nothing. There is no window to open one in; open pages through the mod instead.
DownloadsWork — the file lands in webgui-downloads/ under the game directory and the player is told the name.
window.webgui.assetsBaseSet when the server hosts the page's files; see Pages the server hosts.
window.webgui.isHudtrue 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 shortcutsNot 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:

diff
- import { useWebGUIClient } from '@webgui/react'
+ import { useWebGUIClient } from '@webgui/client/react'
diff
- import { useWebGUIClient } from '@webgui/vue'
+ import { useWebGUIClient } from '@webgui/client/vue'
diff
- 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.