Getting Started with Custom UIs in Hytale

tutorial java plugin ui beginner

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:

PropertyExamplePurpose
GroupGroup { }Container element
LabelLabel { }Text display
Anchor(Width: 400, Height: 200)Size and positioning
Background#1a1a2e(0.95)Color with alpha
LayoutModeTopStack 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

ModeDirection
TopStack vertically, top to bottom
BottomStack vertically, bottom to top
LeftStack horizontally, left to right
RightStack 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_):

  • CustomPage packet - sends UI files and commands to the client
  • CustomPageEvent packet - receives user interactions from the client
  • CustomPageLifetime enum - controls dismissal behavior

Builder Classes (com.hypixel.hytale.server.core.ui.builder):

  • UICommandBuilder - builds commands like append, set, clear, remove
  • UIEventBuilder - 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:

ComponentPurpose
GroupContainer with layout
LabelText display
TextButtonButton with text
ButtonIcon button
TextFieldText input
NumberFieldNumeric input
CheckBoxBoolean toggle
DropdownBoxSelection list
SpriteImage/animation
MultilineTextFieldMulti-line text input
ColorPickerColor selection

Common Mistakes

UI file not loading

Check:

  • Path in cmd.append() is relative to Common/UI/Custom/
  • manifest.json has "IncludesAssetPack": true
  • build.gradle.kts includes from("src/main/resources")
Invalid property errors

These properties do NOT exist:

  • ClipChildren
  • Margin (use Padding instead)
  • 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

ClassUse Case
BasicCustomUIPageStatic display, no events
InteractiveCustomUIPage<T>Handles user interactions

CustomPageLifetime

ValueBehavior
CanDismissPlayer can close with ESC
CantClosePlayer cannot close
CanDismissOrCloseThroughInteractionESC or UI button

UICommandBuilder Methods

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


Happy building!