Skip to content

UI Building Tools

The UI building tools provide a fluent API for constructing and updating user interfaces from server-side code.

Build UI manipulation commands to send to the client.

import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
UICommandBuilder builder = new UICommandBuilder();

These are the available CustomUICommandType enum values from com.hypixel.hytale.protocol.packets.interface_:

TypeDescription
AppendAdd elements from a document path to root or a selector
AppendInlineAdd elements from inline UI definition to a selector
InsertBeforeInsert elements from a document path before a selector
InsertBeforeInlineInsert inline elements before a selector
RemoveRemove elements matching selector
SetSet property value on elements
ClearClear children of elements matching selector
import com.hypixel.hytale.protocol.packets.interface_.CustomUICommandType;
// Append UI document to page root
builder.append("Pages/MyPage.ui");
// Append to specific container
builder.append("#container", "Components/Button.ui");
// Append inline UI markup (uses Hytale's UI markup syntax)
builder.appendInline("#container", "Label { Text: No items found; Style: (Alignment: Center); }");

Important: The server does not validate inline UI markup; it is sent verbatim to the client. For complex layouts with images, custom panels, or backgrounds, prefer a .ui file in your plugin’s asset pack.

// Insert before an element (recommended for complex UI)
builder.insertBefore("#target-element", "Components/Header.ui");
// Insert inline before an element (client-side markup string)
builder.insertBeforeInline("#target-element", "Label { Text: Header; Style: (FontSize: 18); }");
// Remove element from DOM entirely
builder.remove("#old-element");
// Clear element's children but keep element
builder.clear("#container");
// Set text content
builder.set("#health-text", "100 HP");
// Set Message objects (for localized/formatted text)
builder.set("#greeting", message);
// Set numeric values
builder.set("#health-bar", 0.75f); // float
builder.set("#count", 42); // int
builder.set("#value", 3.14); // double
// Set boolean values
builder.set("#is-visible", true);
// Set null value
builder.setNull("#optional-field");
// Set complex objects (must have a registered codec)
builder.setObject("#item-slot", itemGridSlot);
// Set arrays of compatible types
builder.set("#items", itemStackArray);
// Set lists of compatible types
builder.set("#items", itemStackList);

The set() method has dedicated overloads for these primitive types:

  • String - Text values
  • boolean - Boolean values
  • int - Integer values
  • float - Float values
  • double - Double values
  • Message - Localized/formatted messages (com.hypixel.hytale.server.core.Message)
  • Value<T> - References to values in other UI documents

For setObject(), the following types are registered in the CODEC_MAP:

TypePackage
Areacom.hypixel.hytale.server.core.ui.Area
ItemGridSlotcom.hypixel.hytale.server.core.ui.ItemGridSlot
ItemStackcom.hypixel.hytale.server.core.inventory.ItemStack
LocalizableStringcom.hypixel.hytale.server.core.ui.LocalizableString
PatchStylecom.hypixel.hytale.server.core.ui.PatchStyle
DropdownEntryInfocom.hypixel.hytale.server.core.ui.DropdownEntryInfo
Anchorcom.hypixel.hytale.server.core.ui.Anchor

These types can also be used in arrays (T[]) or lists (List<T>) with the set() method.

Reference values from other UI documents using the Value class:

import com.hypixel.hytale.server.core.ui.Value;
// Reference a style from Common.ui
builder.set("#button.Style", Value.ref("Common.ui", "DefaultButtonStyle"));
// Reference with nested path
builder.set("#panel.Theme", Value.ref("Themes.ui", "DarkTheme.Colors"));

The Value class supports two factory methods:

  • Value.ref(String documentPath, String valueName) - Create a reference to a value in another UI document
  • Value.of(T value) - Wrap a direct value (note: cannot be used with set() which only accepts references)
// Get array of commands to send
CustomUICommand[] commands = builder.getCommands();
// Empty array constant (useful when no commands needed)
CustomUICommand[] empty = UICommandBuilder.EMPTY_COMMAND_ARRAY;

Bind UI events to server-side handlers.

import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
UIEventBuilder eventBuilder = new UIEventBuilder();

These are the available CustomUIEventBindingType enum values from com.hypixel.hytale.protocol.packets.interface_:

TypeDescription
ActivatingPrimary activation (click/tap)
RightClickingRight mouse button click
DoubleClickingDouble click
MouseEnteredMouse entered element bounds
MouseExitedMouse exited element bounds
ValueChangedValue changed (sliders, inputs)
ElementReorderedElement was reordered in a list
ValidatingValidation event
DismissingPage is being dismissed
FocusGainedElement gained focus
FocusLostElement lost focus
KeyDownKey press event
MouseButtonReleasedMouse button released
SlotClickingSlot click (for inventory-style grids)
SlotDoubleClickingSlot double click
SlotMouseEnteredMouse entered slot
SlotMouseExitedMouse exited slot
DragCancelledDrag operation cancelled
DroppedElement was dropped
SlotMouseDragCompletedSlot drag completed
SlotMouseDragExitedSlot drag exited
SlotClickReleaseWhileDraggingSlot click released while dragging
SlotClickPressWhileDraggingSlot click pressed while dragging
SelectedTabChangedTab selection changed
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
// Simple activation (click) binding
eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#my-button");
// Value change binding for inputs
eventBuilder.addEventBinding(CustomUIEventBindingType.ValueChanged, "#slider");
// With explicit locking behavior (second parameter)
eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#button", true); // locks interface
import com.hypixel.hytale.server.core.ui.builder.EventData;
// Create event data (all values are stored as strings)
EventData data = new EventData()
.append("Action", "submit")
.append("ItemId", "123")
.append("Quantity", "5"); // Note: numeric values must be strings
// Bind with data
eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#submit-btn", data);

By default, events lock the UI until the server responds (the locksInterface parameter defaults to true). For responsive UIs that shouldn’t wait for server response, set locksInterface to false:

// Non-locking event (doesn't block UI while waiting for server)
eventBuilder.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#slider",
data,
false // locksInterface = false
);
// Get array of bindings
CustomUIEventBinding[] bindings = eventBuilder.getEvents();
// Empty array constant (useful when no events needed)
CustomUIEventBinding[] empty = UIEventBuilder.EMPTY_EVENT_BINDING_ARRAY;

EventData is a record that wraps a Map<String, String> for passing key-value parameters with events.

public record EventData(Map<String, String> events) { ... }
import com.hypixel.hytale.server.core.ui.builder.EventData;
// Create empty event data using no-arg constructor
EventData data = new EventData()
.append("Key1", "value1")
.append("Key2", "value2")
.append("Count", "42");
// Create with initial value using factory method
EventData data = EventData.of("Action", "confirm");
// Append enum values (converted to enum name string)
data.append("Direction", Direction.NORTH); // stores "NORTH"
// Create with an existing map
Map<String, String> existingMap = new HashMap<>();
existingMap.put("Key", "value");
EventData data = new EventData(existingMap);
MethodDescription
new EventData()Create empty event data (uses Object2ObjectOpenHashMap internally)
new EventData(Map<String, String>)Create with an existing map
EventData.of(String key, String value)Factory method to create with initial key-value pair
append(String key, String value)Add a string value (returns this for chaining)
append(String key, Enum<?> enumValue)Add an enum value (stored as enumValue.name())
put(String key, String value)Alias for append (returns this for chaining)
events()Get the underlying Map<String, String>

All values are stored as strings in the underlying map:

  • String - Text values (stored directly)
  • Enum<T> - Enum constants (serialized via name() method)

A custom interactive page with event handling:

import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.EventData;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import java.util.List;
public class ShopPage extends InteractiveCustomUIPage<ShopEventData> {
private final List<ShopItem> items;
public ShopPage(PlayerRef playerRef, List<ShopItem> items, BuilderCodec<ShopEventData> codec) {
super(playerRef, CustomPageLifetime.CanDismiss, codec);
this.items = items;
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder commands,
UIEventBuilder events, Store<EntityStore> store) {
// Load page template
commands.append("Pages/ShopPage.ui");
commands.set("#shop-title", "Item Shop");
// Add items dynamically
for (int i = 0; i < items.size(); i++) {
ShopItem item = items.get(i);
// Append item template
commands.append("#item-list", "Components/ShopItem.ui");
commands.set("#item-" + i + "-name", item.getName());
commands.set("#item-" + i + "-price", item.getPrice() + " coins");
// Bind purchase event (note: index must be string)
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#buy-btn-" + i,
EventData.of("Action", "buy").append("Index", String.valueOf(i))
);
}
// Bind close button
events.addEventBinding(CustomUIEventBindingType.Activating, "#close-btn");
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store,
ShopEventData data) {
if ("buy".equals(data.action())) {
int index = Integer.parseInt(data.index());
ShopItem item = items.get(index);
// Process purchase...
// Update UI
UICommandBuilder update = new UICommandBuilder();
update.set("#balance", newBalance + " coins");
sendUpdate(update, null, false);
} else {
close();
}
}
}

The CustomPageLifetime enum controls how the page can be closed:

ValueDescription
CantClosePage cannot be closed by user
CanDismissPage can be dismissed (e.g., pressing Escape)
CanDismissOrCloseThroughInteractionPage can be dismissed or closed through in-game interaction

Hytale uses a custom markup syntax for .ui files and inline UI. This is NOT HTML - it uses a curly-brace format:

ElementType {
PropertyName: value;
PropertyName2: (NestedKey: value; NestedKey2: value);
}

Inline UI markup (appendInline, insertBeforeInline) is interpreted by the client; the server does not validate which element types are accepted. Common examples:

ElementDescriptionExample
LabelText displayLabel { Text: Hello; Style: (Alignment: Center); }
GroupContainerGroup { LayoutMode: Left; Anchor: (Bottom: 0); }
// Simple label
builder.appendInline("#container", "Label { Text: No items found; Style: (Alignment: Center); }");
// Group container
builder.appendInline("#list", "Group { LayoutMode: Left; Anchor: (Bottom: 0); }");
// Localized text (use % prefix for translation keys)
builder.appendInline("#messages", "Label { Text: %customUI.noItems; Style: (Alignment: Center); }");

For complex UI elements (panels, images, buttons), create .ui files in your plugin’s asset pack:

  1. Set "IncludesAssetPack": true in your plugin’s manifest.json
  2. Create .ui files in src/main/resources/Common/UI/Custom/
  3. Reference them using append() or insertBefore()

Directory structure:

src/main/resources/
├── manifest.json # IncludesAssetPack: true
└── Common/
└── UI/
└── Custom/
├── MyCustomPanel.ui # Your .ui files
└── MyBackground.png # Textures

Loading textures: Texture paths in .ui files are relative to the .ui file location. Use PatchStyle() to define textures and apply them as backgrounds:

// Include Common.ui to access built-in styles
$Common = "Common.ui";
// Define a texture variable (path relative to this .ui file)
@MyTex = PatchStyle(TexturePath: "MyBackground.png");
Group {
LayoutMode: Center;
Group #MyPanel {
Background: @MyTex;
Anchor: (Width: 800, Height: 1000);
LayoutMode: Top;
}
}

Important notes:

  • Place textures in the same folder as your .ui file for simplest relative paths
  • The texture will automatically stretch to fit the element size
  • Import Common.ui using $Common = "Common.ui"; to access built-in styles
  • Reference styles with Style: $Common.@DefaultInputFieldStyle;
// Reference the custom .ui file from Java
builder.append("Custom/MyCustomPanel.ui");
  1. Batch updates - Combine multiple set() calls in one builder
  2. Use non-locking events for frequent updates like sliders
  3. Reference styles from Common.ui for consistency
  4. Clear before append when replacing dynamic content
  5. Handle event data validation - clients can send malformed data
  6. Use .ui files for complex layouts - Inline markup is limited to simple elements
  7. Include asset pack for images - Set IncludesAssetPack: true in manifest.json
  8. Place textures with .ui files - Put image files in the same directory as your .ui files for easy relative paths
  9. Use PatchStyle for textures - Define textures with @MyTex = PatchStyle(TexturePath: "file.png"); and apply with Background: @MyTex;