Getting Started with Custom UIs in Hytale
Custom UIs are one of the most powerful features in Hytale server plugins. They let you create menus, forms, HUDs, and interactive panels. In this guide, we’ll build the simplest possible custom UI - a static display page.
Prefer video? Watch this tutorial on YouTube.
Prerequisites
- A working Hytale dev server with
HytaleServer.jar - Basic Java knowledge
- Completed the first plugin tutorial - we’ll build on that foundation
Create the Project
Create a new Gradle project like in the first tutorial. Name it TestUIPlugin with your preferred package structure. I’ll use de.noel.testui.
Your project structure will look like this:
test-ui-plugin/
├── build.gradle.kts
├── libs/
│ └── HytaleServer.jar
└── src/main/
├── java/
│ └── de/noel/testui/
└── resources/
Configure build.gradle.kts
Open build.gradle.kts. Start with the basic plugin setup:
plugins {
java
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
Add the Hytale dependency:
plugins {
java
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
compileOnly(files("libs/HytaleServer.jar"))
}
Now the important part - configure the JAR task to include our UI assets:
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveBaseName.set("TestUIPlugin")
archiveVersion.set("1.0.0")
from("src/main/resources")
}
The highlighted line is crucial - without it, your UI files won’t be bundled in the JAR.
Click to show complete build.gradle.kts
plugins {
java
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
compileOnly(files("libs/HytaleServer.jar"))
}
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveBaseName.set("TestUIPlugin")
archiveVersion.set("1.0.0")
from("src/main/resources")
}
Create the Manifest
Create src/main/resources/manifest.json:
{
"Group": "TestUI",
"Name": "TestUIPlugin",
"Version": "1.0.0",
"Main": "de.noel.testui.TestUIPlugin",
"IncludesAssetPack": true
}
The key difference from a regular plugin: "IncludesAssetPack": true. This tells Hytale that your plugin contains assets (like UI files) that need to be sent to clients.
Create the UI File
Now let’s create our first UI definition. Create the folder structure:
src/main/resources/Common/UI/Custom/Pages/
Inside Pages/, create Tutorial1Page.ui. Let’s build it step by step.
Start with an empty Group - the root container:
Group {
}
Add size and background:
Group {
Anchor: (Width: 400, Height: 200);
Background: #1a1a2e(0.95);
}
Configure the layout - we want children to stack from top to bottom:
Group {
Anchor: (Width: 400, Height: 200);
Background: #1a1a2e(0.95);
LayoutMode: Top;
Padding: (Full: 20);
}
Now add the content - three labels:
Group {
Anchor: (Width: 400, Height: 200);
Background: #1a1a2e(0.95);
LayoutMode: Top;
Padding: (Full: 20);
Label #Title {
Text: "Tutorial Level 1";
Anchor: (Height: 40);
Style: (FontSize: 24, TextColor: #ffffff, Alignment: Center);
}
Label #Subtitle {
Text: "Static Display - No Events";
Anchor: (Height: 30);
Style: (FontSize: 16, TextColor: #888888, Alignment: Center);
}
Label #Info {
Text: "Press ESC to close";
Anchor: (Height: 25);
Style: (FontSize: 14, TextColor: #666666, Alignment: Center);
}
}
Click to show complete Tutorial1Page.ui
Group {
Anchor: (Width: 400, Height: 200);
Background: #1a1a2e(0.95);
LayoutMode: Top;
Padding: (Full: 20);
Label #Title {
Text: "Tutorial Level 1";
Anchor: (Height: 40);
Style: (FontSize: 24, TextColor: #ffffff, Alignment: Center);
}
Label #Subtitle {
Text: "Static Display - No Events";
Anchor: (Height: 30);
Style: (FontSize: 16, TextColor: #888888, Alignment: Center);
}
Label #Info {
Text: "Press ESC to close";
Anchor: (Height: 25);
Style: (FontSize: 14, TextColor: #666666, Alignment: Center);
}
}
Understanding the UI DSL
Let’s break down what we just wrote:
| Property | Example | Purpose |
|---|---|---|
Group | Group { } | Container element |
Label | Label { } | Text display |
Anchor | (Width: 400, Height: 200) | Size and positioning |
Background | #1a1a2e(0.95) | Color with alpha |
LayoutMode | Top | Stack direction |
Padding | (Full: 20) | Inner spacing |
Style | (FontSize: 24, ...) | Text styling |
Colors
Colors use hex format. Add alpha in parentheses:
#ffffff // White, full opacity
#000000(0.5) // Black, 50% opacity
#1a1a2e(0.95) // Dark blue, 95% opacity
Element IDs
The # prefix gives elements an ID:
Label #Title { ... }
You’ll need IDs for dynamic updates and event handling in later tutorials.
Layout Modes
| Mode | Direction |
|---|---|
Top | Stack vertically, top to bottom |
Bottom | Stack vertically, bottom to top |
Left | Stack horizontally, left to right |
Right | Stack horizontally, right to left |
Create the Page Class
Now let’s connect our UI file to Java. Create de.noel.testui.tutorial.level1.Tutorial1Page:
package de.noel.testui.tutorial.level1;
public class Tutorial1Page {
}
Extend BasicCustomUIPage - the simplest page type for static displays:
package de.noel.testui.tutorial.level1;
import com.hypixel.hytale.server.core.entity.entities.player.pages.BasicCustomUIPage;
public class Tutorial1Page extends BasicCustomUIPage {
}
Add the constructor. It takes a PlayerRef (who sees the page) and a CustomPageLifetime (can the player close it?):
package de.noel.testui.tutorial.level1;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.server.core.entity.entities.player.pages.BasicCustomUIPage;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import javax.annotation.Nonnull;
public class Tutorial1Page extends BasicCustomUIPage {
public Tutorial1Page(@Nonnull PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss);
}
}
CustomPageLifetime.CanDismiss means players can press ESC to close the page.
Now override the build method - this is where we load our UI file:
package de.noel.testui.tutorial.level1;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.server.core.entity.entities.player.pages.BasicCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import javax.annotation.Nonnull;
public class Tutorial1Page extends BasicCustomUIPage {
public Tutorial1Page(@Nonnull PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss);
}
@Override
public void build(@Nonnull UICommandBuilder cmd) {
cmd.append("Pages/Tutorial1Page.ui");
}
}
The path "Pages/Tutorial1Page.ui" is relative to src/main/resources/Common/UI/Custom/.
Click to show complete Tutorial1Page.java
package de.noel.testui.tutorial.level1;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.server.core.entity.entities.player.pages.BasicCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import javax.annotation.Nonnull;
/**
* Tutorial Level 1: Static Display
*
* The simplest possible custom UI page.
* - Extends BasicCustomUIPage (no event handling)
* - Just loads a .ui file and displays it
*/
public class Tutorial1Page extends BasicCustomUIPage {
public Tutorial1Page(@Nonnull PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss);
}
@Override
public void build(@Nonnull UICommandBuilder cmd) {
// Load the UI file
// Path is relative to: src/main/resources/Common/UI/Custom/
cmd.append("Pages/Tutorial1Page.ui");
}
}
Create the Command
Create de.noel.testui.tutorial.level1.Tutorial1Command to open the page:
package de.noel.testui.tutorial.level1;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import javax.annotation.Nonnull;
public class Tutorial1Command extends AbstractPlayerCommand {
public Tutorial1Command() {
super("tutorial1", "Opens the Tutorial 1 page", false);
}
}
Add the execute method:
Click to show imports
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
@Override
protected void execute(
@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
Player player = store.getComponent(ref, Player.getComponentType());
Tutorial1Page page = new Tutorial1Page(playerRef);
player.getPageManager().openCustomPage(ref, store, page);
}
The key line is openCustomPage - this sends the UI to the client and displays it.
Click to show complete Tutorial1Command.java
package de.noel.testui.tutorial.level1;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
public class Tutorial1Command extends AbstractPlayerCommand {
public Tutorial1Command() {
super("tutorial1", "Opens the Tutorial 1 page", false);
}
@Override
protected void execute(
@Nonnull CommandContext ctx,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
Player player = store.getComponent(ref, Player.getComponentType());
Tutorial1Page page = new Tutorial1Page(playerRef);
player.getPageManager().openCustomPage(ref, store, page);
}
}
Create the Main Plugin Class
Create de.noel.testui.TestUIPlugin:
package de.noel.testui;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import de.noel.testui.tutorial.level1.Tutorial1Command;
import javax.annotation.Nonnull;
public class TestUIPlugin extends JavaPlugin {
public TestUIPlugin(@Nonnull JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
super.setup();
this.getCommandRegistry().registerCommand(new Tutorial1Command());
}
}
Build and Test
Build the JAR:
./gradlew jar
Copy build/libs/TestUIPlugin-1.0.0.jar to your server’s mods folder and restart.
In-game, run:
/tutorial1
You should see your custom UI panel. Press ESC to close it.
How It Works
Here’s the flow when a player runs /tutorial1:
┌─────────────────────────────────────────────────────────┐
│ Server │
├─────────────────────────────────────────────────────────┤
│ 1. Tutorial1Command.execute() │
│ ↓ │
│ 2. new Tutorial1Page(playerRef) │
│ ↓ │
│ 3. page.build(cmd) │
│ ↓ │
│ 4. cmd.append("Pages/Tutorial1Page.ui") │
│ ↓ │
│ 5. CustomPage packet sent to client │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Client │
├─────────────────────────────────────────────────────────┤
│ 6. Receives packet │
│ ↓ │
│ 7. Loads Tutorial1Page.ui from plugin assets │
│ ↓ │
│ 8. Parses UI DSL and renders to screen │
└─────────────────────────────────────────────────────────┘
Where Does the UI DSL Come From?
You might wonder: where is this UI language defined? Can I find documentation?
What We Know
The UI DSL is parsed client-side. The server only sends commands like “load this .ui file” or “set this property” - the actual interpretation happens in the Hytale client.
Important: There’s no official documentation for the UI DSL yet. Property names and valid values are discovered through trial and error, analyzing Hytale’s built-in UI files, and checking the decompiled server code. Some properties have multiple valid forms (e.g., Alignment vs HorizontalAlignment) - when in doubt, look at working examples in Assets/Common/UI/.
In the decompiled server code, we can find:
Protocol Layer (com.hypixel.hytale.protocol.packets.interface_):
CustomPagepacket - sends UI files and commands to the clientCustomPageEventpacket - receives user interactions from the clientCustomPageLifetimeenum - controls dismissal behavior
Builder Classes (com.hypixel.hytale.server.core.ui.builder):
UICommandBuilder- builds commands likeappend,set,clear,removeUIEventBuilder- binds events to elements
24 Event Types (CustomUIEventBindingType):
Activating, RightClicking, DoubleClicking, MouseEntered,
MouseExited, ValueChanged, ElementReordered, Validating,
Dismissing, FocusGained, FocusLost, KeyDown, SlotClicking,
SelectedTabChanged, and more...
Built-in UI Examples
Hytale’s own UIs use the same system. In the game assets, you can find:
Assets/Common/UI/Custom/
├── Common.ui # Base styles and components
├── Common/
│ ├── TextButton.ui
│ ├── ActionButton.ui
│ └── ...
├── Pages/
│ ├── ShopPage.ui
│ ├── QuestPage.ui
│ └── ...
└── Hud/
└── ...
The Common.ui file defines reusable styles like:
@DefaultLabelStyle = (FontSize: 16, TextColor: #96a9be);
@DefaultButtonHeight = 44;
@TextButton = TextButton {
Anchor: (Height: @DefaultButtonHeight);
...
};
Importing from Common.ui
You can import Hytale’s built-in components using:
$C = "../Common.ui";
Then use components like $C.@TextField:
$C.@TextField #NameInput {
Anchor: (Height: 40);
PlaceholderText: "Type here...";
}
This gives you access to pre-styled components. However, for this first tutorial we’re keeping things simple and defining everything inline.
Available Components
From analyzing the codebase, these components exist:
| Component | Purpose |
|---|---|
Group | Container with layout |
Label | Text display |
TextButton | Button with text |
Button | Icon button |
TextField | Text input |
NumberField | Numeric input |
CheckBox | Boolean toggle |
DropdownBox | Selection list |
Sprite | Image/animation |
MultilineTextField | Multi-line text input |
ColorPicker | Color selection |
Common Mistakes
UI file not loading
Check:
- Path in
cmd.append()is relative toCommon/UI/Custom/ manifest.jsonhas"IncludesAssetPack": truebuild.gradle.ktsincludesfrom("src/main/resources")
Invalid property errors
These properties do NOT exist:
ClipChildrenMargin(usePaddinginstead)LayoutMode: Center(use FlexWeight spacers)
What’s Next?
This was Level 1 - static display with no interaction. Future tutorials will cover:
- Level 2: Button clicks and event handling
- Level 3: Dynamic lists
- Level 4: Live UI updates
- Level 5: Multi-page navigation
- Level 6: Text input forms
Quick Reference
Page Types
| Class | Use Case |
|---|---|
BasicCustomUIPage | Static display, no events |
InteractiveCustomUIPage<T> | Handles user interactions |
CustomPageLifetime
| Value | Behavior |
|---|---|
CanDismiss | Player can close with ESC |
CantClose | Player cannot close |
CanDismissOrCloseThroughInteraction | ESC or UI button |
UICommandBuilder Methods
| Method | Purpose |
|---|---|
append(path) | Load UI file |
set(selector, value) | Update property |
clear(selector) | Clear container children |
remove(selector) | Remove element |
Source Code
The complete source code for this tutorial is available on GitHub:
- hytale-basic-uis - The final result with Tutorial 1, 2, and 3
- noels-whitelist-manager - A complete plugin with more complex UIs, dynamic updates, and page navigation
Happy building!