The Overlay UI is exactly what it sounds like. A UI that overlays your game. You can use it to create any type of UI you'd like.
Overlay UI is loaded into an absolutely positioned <div> on top of the game scene that spans the full width and height of the window.
All Overlay UI is defined in the .html file used to load your UI when invoking player.ui.load()for a player from the server.
We'll use the Overlay UI to create a skills based UI that looks like something that would belong in an RPG (role-playing game) as an overlay in our game.
First, let's make sure we've created our index.htmlfile at assets/ui/index.html.
In our index.html, we'll add the following HTML & CSS to create our UI. In the same exact way you build standard web pages, you can build your HYTOPIA Overlay UI.
Now, on our server, when a player joins our game we'll load the UI file we created for them.
We can do that as follows.
That's it! That's how easy it is to create a basic Overlay UI in HYTOPIA!
If all went well, we should see an awesome skills menu that looks like this.
In our previous example, we created our skills menu UI, but it doesn't update or change based on gameplay.
Let's expand on it so that we can communicate things like updates to the player's skill levels from the server.
To do this, we need our UI to listen for data from the server. Here's how we can update our index.htmlfile to listen for this data. Let's add this script to the top of the file
Perfect, our UI is ready to listen for data from our server.
For the sake of showcasing how data works, let's do something simple like set the level of our player's different skills to a random value every second, controlled by the server.
Here's how we can do that.
That's it! That's all we have to do to send data to the UI of a specific player.
Here's what we should see in our Overlay UI.
Let's end on one final example. We're sending data down to our UI, but what if we need to send data from our UI back to our server?
We can do that as well, and receive that data on the server with a reference to the player it came from, allowing us to fully scope any UI and game behavior specific to each player if necessary.
In our index.htmlfile for our UI that we created in the previous example, we'll add the following to our script.
Now, on our server we can listen for data from our player like this.
Now, in our server console, we should see a console.log every second for the data sent up from the UI.
That's it! You can expand on these concepts of sending and receiving data however you'd like. The interface for data communication was left intentionally simple and uses generic JSON compatible objects to allow you to create whatever data structures and interactions you need for your specific game.
If you need to programmatically unlock a player's cursor lock, which hides their cursor while they're controlling their character in game, we can do that directly from our server code.
By default, a user must press Escapeor Tto unlock their cursor to interact with UI elements. This isn't a great user experience if a menu suddenly pops up for your game, or they interact with something in game that results in a UI change that also requires interaction. They'd have to manually unlock their own pointer with Escape or T, and that's annoying.
On our server, we can lock and unlock a player's cursor at any time with the following code.
Simple! Now, we can better control the UI experience of a player based on everything from in game interactions, UI interactions, and more.
The Overlay UI related systems are constantly evolving. You can find the latest .
If there are features that we don't currently support for Overlay UI that you'd like to see added to the HYTOPIA SDK, you can .
<style>
.skills-panel {
position: fixed;
bottom: 20px;
right: 20px;
background: linear-gradient(to bottom, rgba(40, 40, 40, 0.92), rgba(25, 25, 25, 0.92));
border: 3px solid rgba(180, 180, 180, 0.4);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5),
inset 0 0 20px rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 15px;
width: 220px;
backdrop-filter: blur(4px);
}
.skill-row {
display: flex;
align-items: center;
margin: 8px 0;
padding: 6px 8px;
background: rgba(30, 30, 30, 0.6);
border: 1px solid rgba(200, 200, 200, 0.15);
border-radius: 6px;
transition: all 0.2s ease;
}
.skill-row:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(-3px);
}
.skill-icon {
width: 32px;
height: 32px;
margin-right: 12px;
background-size: contain;
background-repeat: no-repeat;
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.5));
}
.skill-name {
flex: 1;
color: #ffffff;
font-family: 'Trebuchet MS', 'Arial', sans-serif;
font-size: 15px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
letter-spacing: 0.5px;
}
.skill-level {
font-weight: bold;
font-size: 16px;
color: #00ffaa;
text-shadow: 0 0 8px rgba(0, 255, 170, 0.4),
1px 1px 2px rgba(0, 0, 0, 0.8);
font-family: 'Georgia', serif;
padding: 0 6px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.mining-icon {
background-image: url('https://static.wikia.nocookie.net/runescape2/images/0/0a/Mining_shop_map_icon.png');
}
.woodcutting-icon {
background-image: url('https://static.wikia.nocookie.net/runescape2/images/8/81/Axe_shop_map_icon.png');
}
.fishing-icon {
background-image: url('https://static.wikia.nocookie.net/runescape2/images/4/4f/Fishing_shop_map_icon.png');
}
.combat-icon {
background-image: url('https://static.wikia.nocookie.net/runescape2/images/4/4a/Sword_shop_map_icon.png');
}
</style>
<div class="skills-panel">
<div class="skill-row">
<div class="skill-icon mining-icon"></div>
<div class="skill-name">Mining</div>
<div class="skill-level mining-level">13</div>
</div>
<div class="skill-row">
<div class="skill-icon woodcutting-icon"></div>
<div class="skill-name">Woodcutting</div>
<div class="skill-level woodcutting-level">10</div>
</div>
<div class="skill-row">
<div class="skill-icon fishing-icon"></div>
<div class="skill-name">Fishing</div>
<div class="skill-level fishing-level">7</div>
</div>
<div class="skill-row">
<div class="skill-icon combat-icon"></div>
<div class="skill-name">Combat</div>
<div class="skill-level combat-level">22</div>
</div>
</div>world.on(PlayerEvent.JOINED_WORLD, ({ player }) => {
player.ui.load('ui/index.html');
// ... other code
});<!-- Top of our index.html file -->
<script>
hytopia.onData(data => { // data is any arbitrary object you send from the server
if (data.type === 'mining-level') {
document.querySelector('.mining-level').textContent = data.level;
}
if (data.type === 'woodcutting-level') {
document.querySelector('.woodcutting-level').textContent = data.level;
}
if (data.type === 'fishing-level') {
document.querySelector('.fishing-level').textContent = data.level;
}
if (data.type === 'combat-level') {
document.querySelector('.combat-level').textContent = data.level;
}
});
</script>
<!-- ... The rest of our index.html from the previous example ... -->world.on(PlayerEvent.JOINED_WORLD, ({ player }) => {
player.ui.load('ui/demo.html');
// Notice that .sendData is specific to the player. We can
// control sending data uniquely to each individual player as
// needed through their player.ui
setInterval(() => {
player.ui.sendData({ type: 'mining-level', level: Math.floor(Math.random() * 100) });
player.ui.sendData({ type: 'woodcutting-level', level: Math.floor(Math.random() * 100) });
player.ui.sendData({ type: 'fishing-level', level: Math.floor(Math.random() * 100) });
player.ui.sendData({ type: 'combat-level', level: Math.floor(Math.random() * 100) });
}, 1000);
// ... other code
});<!-- Top of our index.html file -->
<script>
// Send "ping" data from our UI to server
// sendData() can send any arbitrary object
// with any JSON compatible data.
setInterval(() => {
hytopia.sendData({ hello: 'world!' });
}, 2000);
//////
hytopia.onData(data => { // data is any arbitrary object you send from the server
if (data.type === 'mining-level') {
document.querySelector('.mining-level').textContent = data.level;
}
if (data.type === 'woodcutting-level') {
document.querySelector('.woodcutting-level').textContent = data.level;
}
if (data.type === 'fishing-level') {
document.querySelector('.fishing-level').textContent = data.level;
}
if (data.type === 'combat-level') {
document.querySelector('.combat-level').textContent = data.level;
}
});
</script>
<!-- ... The rest of our index.html from the previous example ... -->world.on(PlayerEvent.JOINED_WORLD, ({ player }) => {
player.ui.load('ui/demo.html');
setInterval(() => {
player.ui.sendData({ type: 'mining-level', level: Math.floor(Math.random() * 100) });
player.ui.sendData({ type: 'woodcutting-level', level: Math.floor(Math.random() * 100) });
player.ui.sendData({ type: 'fishing-level', level: Math.floor(Math.random() * 100) });
player.ui.sendData({ type: 'combat-level', level: Math.floor(Math.random() * 100) });
}, 1000);
player.ui.on(PlayerUIEvent.DATA, ({ playerUI, data }) => {
console.log('got data from this players UI!', data);
// We can also get the player the data came from by
// playerUI.player if ever needed.
});
// ... other code
});// To unlock their pointer
player.ui.lockPointer(false);
// To lock their pointer
player.ui.lockPointer(true);


It's common for some games to have many different Scene UIs loaded into a game at the same time. This can become difficult to track or manage manually, especially if you need to retrieve and perform updates on specific Scene UI instances.
For this, we have the SceneUIManager that provides ways to quickly retrieve, iterate and update loaded Scene UIs in a world.
The SceneUIManager is used as a singleton and automatically created for a given world instance.
You can access the Scene UI Manager for a world like this:
world.sceneUIManagerThe SceneUIManager exposes a number of ways to get different types of SceneUI instances loaded in a world. Here's a few examples of how you can use it.
The SceneUIManager class is constantly evolving. You can find the latest .
If there are features that we don't currently support for SceneUIManager that you'd like to see added to the HYTOPIA SDK, you can .
One of the toughest parts of game development is crafting great user interfaces (UIs).
HYTOPIA makes this process remarkably simple. You can build anything from basic menus to highly complex interfaces using standard web tools like HTML, CSS, and JavaScript. If you’d rather work with a framework like React or Svelte, that’s no problem—HYTOPIA supports those too!
One strength that sets HYTOPIA apart is its extremely flexible and unopinionated UI system. If you can create a webpage, you can create the UI for your game.
Your user interfaces are injected into the same page (DOM) as the HYTOPIA game scene while enabling your user interfaces and your game’s server to seamlessly exchange game state, interactions, and data in real time.
The HYTOPIA game scene and your injected content is loaded in a CSP (Content-Security Policy) controlled iframe, isolating game behavior from any meaningful user data.
All aspects of custom UIs are handled automatically and internally via a WebSocket connection, ensuring everything stays fast and responsive.
Any interaction you can implement on a webpage can work in your UI!
// Returns an array of all loaded scene ui
// instances for the world
world.sceneUIManager.getAllSceneUIs();
// Returns an array of all loaded scene ui
// instances attached to the provided entity.
world.sceneUIManager.getAllEntityAttachedSceneUIs(someEntity);HYTOPIA supports 2 distinct types of user interfaces. Here's the difference between the two.
Overlay UI - The Overlay UI is the global user interface that overlays the game scene. This is great for things like menus, skill bars, leaderboards, countdown timers, visual effects, and other UI elements that do not require spatial placement in the game scene.
Scene UIs - Scene UIs exist spatially within the 3D game scene itself. They can be attached to entities to follow them, placed at a fixed position, and more. Health bars, entity status icons, quest symbols, NPC messages, and much more are examples of what you could use Scene UIs for.
Both the Overlay UI and Scene UI elements are defined in a single .html file.
In this example, we'll use simple HTML and Javascript to create a basic box on the screen as part of the Overlay UI. As long as your UI is bundled/rendered into an entry .html file, it can be loaded as a HYTOPIA UI regardless of the framework you use.
Make sure to never include <html>, <body>, or <head>tags in your entry .html file!
In your assetsfolder of your project, we'll create a new folder called ui . Within that, create a file called index.html. Your folder and file can be named whatever you like, but for this example we'll keep it simple. Your file should exist at assets/ui/index.html
In your index.htmlfile, let's create a basic box UI - remember, UI's act more or less like a transparently overlayed web page, so we can use HTML, CSS and Javascript for it.
Now, in our server code, specifically in our world.on(PlayerEvent.Joined_WORLD, ({ player }) => { ... }) event listener, we can load the UI for the player when they join.
Boom! That's all we have to do, our UI is fully loaded.
UI is loaded and controlled on a per-player basis, giving you fine grain controls of what each player sees.
If you call player.ui.load() again, it will override the previously loaded UI and load a new UI for the player.
Here's what we should see from our example! A red box positioned in the top right of our screen.
If you want to load images in your UI using things like <img /> tag, or load other file types that exist in your assetsfolder, you'll need prefix their relative file path with the magic value {{CDN_ASSETS_URL}} .
For example, let's say in our index.html we have an image we want to load that exists at assets/images/icon.png . We can correctly load it by setting the srcas follows.
All occurrences of this magic {{CDN_ASSETS_URL}}value are replaced at runtime with the origin server your assets serve from. In local development this origin is your local game server, in production when deployed to HYTOPIA services this will be an arbitrarily assigned CDN url.
You can use this to get the correct URI of any asset type from your assetsdirectory. Stylesheets, images, videos, etc.
Nearly every client UI and game server needs to be able to receive or send some amount of data.
Here's how we can do that.
We can send a JSON compatible object of any shape to a player's client as follows.
When sending data, it's sent to a specific player. If you need to send a data update to all players, you can use the PlayerManager to iterate all connected players and send data. In our index.html file.
To receive data on the client, you can use the hytopiaglobal variable in your index.html that is automatically injected by the client and always available when your UI loads.
It's that simple! You have full control over the way data is sent and received by players, as well as the shape of the data based on the needs of your game.
Similar to how we send data from our server to our client UI, we can send data back from our player's client UI to our server. In our index.html:
We can receive UI data sent from each player's client UI and handle it on the server however we need to with the following:
Scene UIs have their own internally tracked state you can control. Each Scene UIs state is unique to that given instance of the scene UI.
Scene UI state management is similar to the state patterns of React. Each Scene UI instance can have its own internally tracked state defined by you, and updated directly to trigger UI updates of just that scene element without extra logic.
You can learn how to use Scene UI state management in Scene UIs.
HYTOPIA fully supports game UIs on mobile. You can learn more about configuring your game UI to support mobile play here: Mobile
If you're building more complex UIs with modals, fade effects, and a lot of managed state, you'll likely want to use some popular framework like React, Svelte, etc.
To use one of these frameworks to build a UI, you'll simply need to make sure the output bundles to a .html file that excludes <html>, <head>and <body> tags. Add that file to your assetsfolder, and then load it as ui with player.ui.load()
The entire game scene, including the UI is loaded for players in a content security policy (CSP) controlled iframe. This sandboxing blocks all external network requests and access to sensitive user data. It effectively acts as a maximum set of isolation around the game and its UI relative to the connected player and their HYTOPIA account.

<div
style="
width: 100px;
height: 100px;
position: absolute;
top: 100px;
right: 100px;
background-color: #FF0000;
"
/>startServer(world => {
// other code...
world.on(PlayerEvent.JOINED_WORLD, ({ player }) => {
// other code..
player.ui.load('ui/index.html'); // loads relative to assets directory, this resolves to assets/ui/index.html
// other code..
});
// other code...
});
<img src="{{CDN_ASSETS_URL}}/images/icon.png" />player.ui.sendData({
my: 'data',
health: 53,
somethingElse: [ '1', 2, 'three' ]
// any properties of any JSON compatible shape you want!
});<script>
hytopia.onData(data => {
console.log(data); // the data object sent from the server to this client ui
});
</script><script>
hytopia.sendData({
hello: 'world',
clicked: 'button',
something: [ 'else' ]
// any properties of any JSON compatible shape you want!
});
</script>// In our game server scripts, anywhere we have a player object we can set the calblack
// Remember, setting the onData callback more than once for a player will override
// the previous set callback for that player!
// Receiving UI data is processed and unique to each player.
player.ui.on(PlayerUIEvent.DATA, ({ playerUi, data }) => {
// playerUI is the reference to player.ui, the ui of the player the data came from
// You can get the player the data is from if needed with playerUI.player
// The data object sent from the client UI to the server
console.log(data);
}Overlay UI
Learn more about the Overlay UI that overlay the game scene.
Scene UIs
Learn about Scene UIs that exist in 3D space within the game scene.
Scene UI Manager
Learn more about the Scene UI Manager that tracks and allows lookup of Scene UIs in the world.


Scene UIs are created with HTML, CSS and Javascript as UI elements within the game scene itself. They can be positioned spatially, follow entities, and more.
They're incredibly flexible and a fantastic way to add another layer of depth to a game. You can use them to create pretty much anything you can imagine - health bars, status symbols that follow entities, quest direction indicators, and so much more.
All Scene UIs start from a template. A template is defined with the hytopiaglobal's registerSceneUITemplate()method injected into your .html file when it loads in the game client.
Here's an example of how we can create a Scene UI that displays an updating message above a player.
First, we need to register the Scene UI Template with a unique template id the server can reference to tell the client what kind of scene ui to create. We'll give our template an id of my-game-message.
Now that we've defined the Scene UI template, our server can tell the client to create an instance of the SceneUI.
Let's use our SceneUI to create a message above each player that joins our game, setting the message to their username. Then, every second we'll perform a state update unique to each player's specific Scene UI instance we created, showcasing how we can control individual Scene UI state.
You can also provide other options when creating a new instance of SceneUI for different behaviors. You can find the latest .
That's it! Here's the result.
On the server, every Scene UI is represented as an instance of the . These instances have their own stateproperty which is an arbitrary object holding the most recent state specific to that instance.
This state can be updated by using the .setState()method of a SceneUI instance. This method expects an arbitrary object of any shape. It will perform a shallow merge between the values provided to .setState()and the existing state object of the instance.
Invoking .setState()will also send the state update to the client, invoking the onState()callback in our template renderer function defined in our .html file, allowing us to control the logic that changes the visual appearance of the SceneUI in game.
You can remove any SceneUI instance from the game through .unload(). For example
Depending on your game's requirements, you can even make your SceneUI interactable with the player's mouse or for text input within the context of the game scene.
With interactable UI elements, you'll likely want to be able to send data back to the server. in the same way you use hytopia.sendData()to send data in our examples, you can also use it to send data from interactions with a Scene UI instance.
Here's an example. Assume we have a button SceneUI template that we create. When a player clicks that button, we want to send data back to the server, telling the server what specific button was clicked.
and then, on our server, we'll listen for that data, and handle it accordingly to retrieve the correctz SceneUI instance on the server.
When interacting with our button, our console.log() in our server code will log the correct instance of the SceneUI for the button that was clicked.
The SceneUI class is constantly evolving. You can find the latest .
If there are features that we don't currently support for Scene UI that you'd like to see added to the HYTOPIA SDK, you can .
<!-- index.html, SceneUI templates & overlay UI exist here. -->
<!-- Loaded by the server calling something like player.ui.load(`assets/path/to/index.html`) -->
<script>
// The first argument of registerSceneUITempalte is the
// template id assigned to this template. The second argument
// is the renderer function used to create a new instance from this
// template.
//
// In the renderer function, id is the scene ui elements unique id,
// not the template id. onState is a function we can provide an
// onState callback to that will be called anytime the specific
// instance rendered from our template has a state update.
hytopia.registerSceneUITemplate('my-game-message', (id, onState) => {
const template = document.getElementById('my-game-message-template');
const clone = template.content.cloneNode(true);
// caveat here! Because our game message gets appended to the dom
// when we return it from this function,
// using clone.querySelector within onState would return null.
// So, we create a reference variable to the message element (messageElement)
// we intend to update in onState so that we can still properly
// get the element reference.
const messageElement = clone.querySelector('.message');
// invoked when the server sends initial state or a state
// update to this specific scene ui instance created from
// our template.
onState(state => {
messageElement.textContent = state.message;
});
return clone; // important!! We must return an HTMLElement
});
</script>
<!-- Our overlay UI that's part of all the rest of our UI could go here, etc... -->
<template id="my-game-message-template">
<div class="my-game-message">
<p class="message"></p>
</div>
</template>
<style>
.my-game-message {
background: rgba(0, 0, 0, 0.8);
border-radius: 12px;
padding: 12px 20px;
color: white;
text-align: center;
position: relative;
margin-bottom: 15px;
}
.my-game-message:after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid rgba(0, 0, 0, 0.8);
}
.my-game-message p {
font-family: Arial, sans-serif;
user-select: none;
font-size: 14px;
margin: 0;
}
</style>world.on(PlayerEvent.JOINED_WORLD, ({ player }) => {
// Load the UI file for the joined player that we created above
player.ui.load('ui/index.html');
const playerEntity = new DefaultPlayerEntity({
player,
name: 'Player',
});
// Create an instance of our SceneUI by the template
// id we defined in our .html file
const messageSceneUI = new SceneUI({
templateId: 'my-game-message',
attachedToEntity: playerEntity, // It'll follow our entity
state: { message: player.username },
offset: { x: 0, y: 1, z: 0 }, // Offset it up slightly so it's above our head
});
// Update the state of this Scene UI instance every second.
setInterval(() => {
messageSceneUI.setState({
message: `${player.username} | ${Math.random() * 100}`,
});
}, 1000);
// Load the Scene UI in the world
messageSceneUI.load(world);
// Spawn the entity
playerEntity.spawn(world, { x: 0, y: 10, z: 0 });
});// ... other code
const messageSceneUI = new SceneUI({
templateId: 'my-game-message',
attachedToEntity: playerEntity,
state: { message: player.username }, // state isn't required, you can also create stateless scene ui.
offset: { x: 0, y: 1, z: 0 },
});
messageSceneUI.load(world);
setTimeout(() => { // remove our scene UI after 5 seconds
messageSceneUI.unload();
}, 5000);<script>
hytopia.registerSceneUITemplate('game-button', (id, onState) => {
const template = document.getElementById('game-button-template');
const clone = template.content.cloneNode(true);
const buttonElement = clone.querySelector('.button');
buttonElement.onclick = () => {
// Send click event to server
hytopia.sendData({
type: 'button-click',
buttonId: id
});
console.log('clicked button!', id);
};
return clone;
});
</script>player.ui.on(PlayerUIEvent.DATA, ({ playerUI, data }) => {
console.log('got data from this players UI!', data);
if (data.type === 'button-click') {
const buttonId = data.buttonId as number;
const sceneUI = world.sceneUIManager.getSceneUIById(buttonId);
console.log('got scene ui!', sceneUI);
// do whatever we want for the click.
}
});
