Skip to content

Custom Pages System

The Custom Pages System allows you to create fully customizable GUI interfaces for players. This includes interactive dialogs, settings pages, choice menus, shop interfaces, and more.

PageManager (Per-Player)
├── Standard Pages (Page enum)
│ └── None, Bench, Inventory, ToolsSettings, Map, etc.
└── Custom Pages (CustomUIPage hierarchy)
├── BasicCustomUIPage - Simple display-only pages
├── InteractiveCustomUIPage<T> - Pages with event handling
└── ChoiceBasePage - Choice/dialog pages
UICommandBuilder - Build UI element commands
UIEventBuilder - Bind events to UI elements
EventData - Pass data with events
Value<T> - Reference UI document values

The PageManager handles opening, closing, and updating pages for each player. Access it through the Player component.

MethodDescription
setPage(ref, store, page)Set a standard page (from Page enum)
setPage(ref, store, page, canClose)Set page with close-through-interaction option
openCustomPage(ref, store, customPage)Open a custom UI page
openCustomPageWithWindows(ref, store, page, windows...)Open custom page with inventory windows
updateCustomPage(customPage)Send updates to the current custom page
handleEvent(ref, store, event)Process incoming page events
getCustomPage()Get the currently open custom page

The Page enum defines built-in page types:

ValueDescription
NoneNo page open (closes current page)
BenchCrafting bench interface
InventoryPlayer inventory
ToolsSettingsTool settings interface
MapWorld map
MachinimaEditorMachinima editing tools
ContentCreationContent creation tools
CustomCustom page (used internally)

The PageManager uses an acknowledgment system to ensure UI updates are processed in order. When a custom page is opened or updated, the client must acknowledge receipt before data events are processed. This prevents race conditions between UI updates and user interactions.

// Internal tracking - handled automatically
private final AtomicInteger customPageRequiredAcknowledgments = new AtomicInteger();
Player playerComponent = store.getComponent(ref, Player.getComponentType());
PageManager pageManager = playerComponent.getPageManager();
// Open inventory
pageManager.setPage(ref, store, Page.Inventory);
// Open page that can be closed by clicking elsewhere
pageManager.setPage(ref, store, Page.Bench, true);
// Close any open page
pageManager.setPage(ref, store, Page.None);

The abstract base class for all custom pages.

public abstract class CustomUIPage {
protected final PlayerRef playerRef;
protected CustomPageLifetime lifetime;
// Must implement - builds the initial page UI
public abstract void build(
Ref<EntityStore> ref,
UICommandBuilder commandBuilder,
UIEventBuilder eventBuilder,
Store<EntityStore> store
);
// Override to handle raw data events (use InteractiveCustomUIPage instead)
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, String rawData);
// Override for cleanup when page is dismissed
public void onDismiss(Ref<EntityStore> ref, Store<EntityStore> store);
// Rebuild the entire page UI
protected void rebuild();
// Send partial updates to the page
protected void sendUpdate();
protected void sendUpdate(UICommandBuilder commandBuilder);
protected void sendUpdate(UICommandBuilder commandBuilder, boolean clear);
// Close this page (sets page to None)
protected void close();
}

Controls how the page can be closed:

ValueDescription
CantClosePlayer cannot close the page (e.g., death screen)
CanDismissPlayer can dismiss with escape key
CanDismissOrCloseThroughInteractionCan dismiss or close by clicking outside

For simple pages that don’t need event handling:

public abstract class BasicCustomUIPage extends CustomUIPage {
public BasicCustomUIPage(PlayerRef playerRef, CustomPageLifetime lifetime) {
super(playerRef, lifetime);
}
// Simplified build method - no event builder needed
public abstract void build(UICommandBuilder commandBuilder);
}

For pages that handle user interactions. The generic type T represents your event data class:

public abstract class InteractiveCustomUIPage<T> extends CustomUIPage {
protected final BuilderCodec<T> eventDataCodec;
public InteractiveCustomUIPage(
PlayerRef playerRef,
CustomPageLifetime lifetime,
BuilderCodec<T> eventDataCodec
) {
super(playerRef, lifetime);
this.eventDataCodec = eventDataCodec;
}
// Override to handle typed event data
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, T data);
// Extended sendUpdate with event builder support
protected void sendUpdate(
UICommandBuilder commandBuilder,
UIEventBuilder eventBuilder,
boolean clear
);
}

Build UI manipulation commands to send to the client.

TypeDescription
AppendAdd elements from a document path
AppendInlineAdd elements from inline UI definition
InsertBeforeInsert elements before a selector
InsertBeforeInlineInsert inline elements before a selector
RemoveRemove elements matching selector
SetSet property value on elements
ClearClear children of elements
UICommandBuilder builder = new UICommandBuilder();
// Append UI document to page or container
builder.append("Pages/MyPage.ui");
builder.append("#Container", "Components/Button.ui");
// Append inline UI definition
builder.appendInline("#List", "Label { Text: Hello; }");
// Insert before element
builder.insertBefore("#ExistingElement", "Components/Header.ui");
builder.insertBeforeInline("#Footer", "Divider { }");
// Remove elements
builder.remove("#ElementToRemove");
// Clear container children
builder.clear("#ListContainer");
// Set property values
builder.set("#Label.Text", "Hello World");
builder.set("#Checkbox.Value", true);
builder.set("#Slider.Value", 50);
builder.set("#Input.Value", 3.14);
builder.set("#Element.Visible", false);
// Set with Message (for localization)
builder.set("#Title.TextSpans", Message.translation("my.translation.key"));
builder.set("#Desc.TextSpans", Message.raw("Plain text"));
// Set null value
builder.setNull("#OptionalField.Value");
// Set arrays
builder.set("#Dropdown.Entries", dropdownEntries);
// Set with Value reference (for document references)
builder.set("#Button.Style", Value.ref("Common/Button.ui", "DefaultStyle"));

Bind events to UI elements so the server receives callbacks when users interact.

TypeDescription
ActivatingElement clicked/activated
RightClickingRight mouse button click
DoubleClickingDouble click
MouseEnteredMouse enters element
MouseExitedMouse leaves element
ValueChangedInput value changed
ElementReorderedDrag-reorder completed
ValidatingInput validation
DismissingPage being dismissed
FocusGainedElement gained focus
FocusLostElement lost focus
KeyDownKey pressed
MouseButtonReleasedMouse button released
SlotClickingInventory slot clicked
SlotDoubleClickingInventory slot double-clicked
SlotMouseEnteredMouse enters slot
SlotMouseExitedMouse exits slot
DragCancelledDrag operation cancelled
DroppedDrop completed
SelectedTabChangedTab selection changed
UIEventBuilder eventBuilder = new UIEventBuilder();
// Simple event binding (locks interface while processing)
eventBuilder.addEventBinding(CustomUIEventBindingType.Activating, "#Button");
// With custom data
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
"#SaveButton",
EventData.of("Action", "Save")
);
// Without interface lock (for real-time updates)
eventBuilder.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#SearchInput",
EventData.of("@Query", "#SearchInput.Value"),
false // Don't lock interface
);
// Complex event data with multiple fields
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
"#SubmitButton",
new EventData()
.append("Action", "Submit")
.append("@Name", "#NameInput.Value")
.append("@Amount", "#AmountSlider.Value")
.append("@Enabled", "#EnableCheckbox.Value"),
true // Lock interface
);
  • Static keys (e.g., "Action", "Index") - Sent as literal string values
  • Reference keys (prefixed with @, e.g., "@Name") - Reference UI element values at event time

Create key-value pairs to send with events.

// Single key-value
EventData data = EventData.of("Action", "Save");
// Multiple values with chaining
EventData data = new EventData()
.append("Type", "Update")
.append("Index", "5")
.append("@Value", "#Input.Value");
// Enum values
EventData data = new EventData()
.append("Mode", MyEnum.OPTION_A); // Sends enum name as string

Reference values from UI documents or provide direct values.

// Reference a value defined in a UI document
Value<String> styleRef = Value.ref("Common/Button.ui", "DefaultStyle");
// Direct value
Value<String> directValue = Value.of("my-value");
// Use with UICommandBuilder.set() for references
commandBuilder.set("#Button.Style", styleRef);

A specialized interactive page for presenting choices to players, commonly used for shops, dialogs, and selection menus.

public abstract class ChoiceBasePage extends InteractiveCustomUIPage<ChoicePageEventData> {
private final ChoiceElement[] elements;
private final String pageLayout;
public ChoiceBasePage(PlayerRef playerRef, ChoiceElement[] elements, String pageLayout) {
super(playerRef, CustomPageLifetime.CanDismiss, ChoicePageEventData.CODEC);
// ...
}
}

Base class for choice options:

public abstract class ChoiceElement {
protected String displayNameKey; // Localization key for display name
protected String descriptionKey; // Localization key for description
protected ChoiceInteraction[] interactions; // Actions when selected
protected ChoiceRequirement[] requirements; // Requirements to select
// Implement to render the choice button
public abstract void addButton(
UICommandBuilder commandBuilder,
UIEventBuilder eventBuilder,
String selector,
PlayerRef playerRef
);
// Check if player meets requirements
public boolean canFulfillRequirements(Store<EntityStore> store, Ref<EntityStore> ref, PlayerRef playerRef);
}

Actions executed when a choice is selected:

public abstract class ChoiceInteraction {
public abstract void run(Store<EntityStore> store, Ref<EntityStore> ref, PlayerRef playerRef);
}

Conditions that must be met to select a choice:

public abstract class ChoiceRequirement {
public abstract boolean canFulfillRequirement(
Store<EntityStore> store,
Ref<EntityStore> ref,
PlayerRef playerRef
);
}

Event data sent when a choice is selected:

public static class ChoicePageEventData {
private int index; // Index of selected choice
public int getIndex() {
return this.index;
}
}

A simple page that displays information without interaction:

import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.server.core.Message;
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;
public class WelcomePage extends BasicCustomUIPage {
private static final String LAYOUT = "Pages/WelcomePage.ui";
private final String playerName;
private final int onlineCount;
public WelcomePage(PlayerRef playerRef, String playerName, int onlineCount) {
super(playerRef, CustomPageLifetime.CanDismiss);
this.playerName = playerName;
this.onlineCount = onlineCount;
}
@Override
public void build(UICommandBuilder commandBuilder) {
commandBuilder.append(LAYOUT);
commandBuilder.set("#Title.TextSpans",
Message.translation("welcome.title").param("player", playerName));
commandBuilder.set("#OnlineCount.Text", String.valueOf(onlineCount));
commandBuilder.set("#ServerTime.Text", java.time.LocalTime.now().toString());
}
}

Opening the page:

Player playerComponent = store.getComponent(ref, Player.getComponentType());
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
WelcomePage page = new WelcomePage(playerRef, player.getDisplayName(), onlinePlayerCount);
playerComponent.getPageManager().openCustomPage(ref, store, page);

A page with form inputs and event handling:

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
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.protocol.packets.interface_.Page;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.entity.entities.Player;
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;
public class PlayerSettingsPage extends InteractiveCustomUIPage<PlayerSettingsPage.SettingsEventData> {
private static final String LAYOUT = "Pages/PlayerSettings.ui";
private boolean notificationsEnabled;
private int renderDistance;
public PlayerSettingsPage(PlayerRef playerRef, boolean notifications, int renderDistance) {
super(playerRef, CustomPageLifetime.CanDismissOrCloseThroughInteraction, SettingsEventData.CODEC);
this.notificationsEnabled = notifications;
this.renderDistance = renderDistance;
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder commandBuilder,
UIEventBuilder eventBuilder, Store<EntityStore> store) {
commandBuilder.append(LAYOUT);
// Set initial values
commandBuilder.set("#NotificationsToggle.Value", notificationsEnabled);
commandBuilder.set("#RenderDistanceSlider.Value", renderDistance);
commandBuilder.set("#RenderDistanceLabel.Text", String.valueOf(renderDistance));
// Bind events
eventBuilder.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#RenderDistanceSlider",
EventData.of("@RenderDistance", "#RenderDistanceSlider.Value"),
false // Don't lock - allow real-time updates
);
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
"#SaveButton",
new EventData()
.append("Action", "Save")
.append("@Notifications", "#NotificationsToggle.Value")
.append("@RenderDistance", "#RenderDistanceSlider.Value")
);
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
"#CancelButton",
EventData.of("Action", "Cancel")
);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store,
SettingsEventData data) {
// Handle real-time slider updates
if (data.renderDistance != null && data.action == null) {
UICommandBuilder builder = new UICommandBuilder();
builder.set("#RenderDistanceLabel.Text", String.valueOf(data.renderDistance));
sendUpdate(builder);
return;
}
// Handle button actions
if (data.action == null) return;
Player playerComponent = store.getComponent(ref, Player.getComponentType());
switch (data.action) {
case "Save":
// Save settings
this.notificationsEnabled = data.notifications != null && data.notifications;
this.renderDistance = data.renderDistance != null ? data.renderDistance : 8;
// Apply settings to player...
playerComponent.sendMessage(Message.translation("settings.saved"));
playerComponent.getPageManager().setPage(ref, store, Page.None);
break;
case "Cancel":
playerComponent.getPageManager().setPage(ref, store, Page.None);
break;
}
}
// Event data class with codec
public static class SettingsEventData {
public static final BuilderCodec<SettingsEventData> CODEC = ((BuilderCodec.Builder)
((BuilderCodec.Builder)((BuilderCodec.Builder)BuilderCodec.builder(
SettingsEventData.class, SettingsEventData::new)
.append(new KeyedCodec<>("Action", Codec.STRING),
(d, v) -> d.action = v, d -> d.action).add())
.append(new KeyedCodec<>("@Notifications", Codec.BOOLEAN),
(d, v) -> d.notifications = v, d -> d.notifications).add())
.append(new KeyedCodec<>("@RenderDistance", Codec.INTEGER),
(d, v) -> d.renderDistance = v, d -> d.renderDistance).add())
.build();
private String action;
private Boolean notifications;
private Integer renderDistance;
}
}

A dialog presenting multiple choices to the player:

import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.entity.entities.player.pages.choices.ChoiceBasePage;
import com.hypixel.hytale.server.core.entity.entities.player.pages.choices.ChoiceElement;
import com.hypixel.hytale.server.core.entity.entities.player.pages.choices.ChoiceInteraction;
import com.hypixel.hytale.server.core.entity.entities.player.pages.choices.ChoiceRequirement;
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;
public class QuestDialogPage extends ChoiceBasePage {
public QuestDialogPage(PlayerRef playerRef, String questId) {
super(playerRef, createChoices(questId), "Pages/QuestDialog.ui");
}
private static ChoiceElement[] createChoices(String questId) {
return new ChoiceElement[] {
new QuestChoiceElement(
"quest.accept.title",
"quest.accept.description",
new ChoiceInteraction[] { new AcceptQuestInteraction(questId) },
null // No requirements
),
new QuestChoiceElement(
"quest.decline.title",
"quest.decline.description",
new ChoiceInteraction[] { new DeclineQuestInteraction(questId) },
null
),
new QuestChoiceElement(
"quest.later.title",
"quest.later.description",
new ChoiceInteraction[] { new ClosePageInteraction() },
null
)
};
}
}
// Custom choice element
class QuestChoiceElement extends ChoiceElement {
public QuestChoiceElement(String displayKey, String descKey,
ChoiceInteraction[] interactions,
ChoiceRequirement[] requirements) {
super(displayKey, descKey, interactions, requirements);
}
@Override
public void addButton(UICommandBuilder commandBuilder, UIEventBuilder eventBuilder,
String selector, PlayerRef playerRef) {
commandBuilder.append(selector, "Components/QuestChoiceButton.ui");
commandBuilder.set(selector + " #Title.TextSpans",
Message.translation(displayNameKey));
commandBuilder.set(selector + " #Description.TextSpans",
Message.translation(descriptionKey));
}
}
// Interaction implementations
class AcceptQuestInteraction extends ChoiceInteraction {
private final String questId;
public AcceptQuestInteraction(String questId) {
this.questId = questId;
}
@Override
public void run(Store<EntityStore> store, Ref<EntityStore> ref, PlayerRef playerRef) {
// Start the quest for the player
// QuestManager.startQuest(playerRef, questId);
playerRef.sendMessage(Message.translation("quest.accepted"));
}
}
class DeclineQuestInteraction extends ChoiceInteraction {
private final String questId;
public DeclineQuestInteraction(String questId) {
this.questId = questId;
}
@Override
public void run(Store<EntityStore> store, Ref<EntityStore> ref, PlayerRef playerRef) {
playerRef.sendMessage(Message.translation("quest.declined"));
}
}
class ClosePageInteraction extends ChoiceInteraction {
@Override
public void run(Store<EntityStore> store, Ref<EntityStore> ref, PlayerRef playerRef) {
// Page will close automatically after interaction
}
}

For pages triggered by block/entity interactions, use a supplier:

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.entity.InteractionContext;
import com.hypixel.hytale.server.core.entity.entities.player.pages.CustomUIPage;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.server.OpenCustomUIInteraction;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
public class MyPageSupplier implements OpenCustomUIInteraction.CustomPageSupplier {
public static final BuilderCodec<MyPageSupplier> CODEC = ((BuilderCodec.Builder)
BuilderCodec.builder(MyPageSupplier.class, MyPageSupplier::new)
.appendInherited(new KeyedCodec<>("ConfigId", Codec.STRING),
(data, o) -> data.configId = o,
data -> data.configId,
(data, parent) -> data.configId = parent.configId)
.add())
.build();
protected String configId;
@Override
public CustomUIPage tryCreate(Ref<EntityStore> ref,
ComponentAccessor<EntityStore> componentAccessor,
PlayerRef playerRef,
InteractionContext context) {
// Create and return the page instance
return new MyConfigPage(playerRef, configId);
}
}

Send incremental changes without rebuilding the entire page:

// In your InteractiveCustomUIPage subclass
private void updateScore(int newScore) {
UICommandBuilder builder = new UICommandBuilder();
builder.set("#ScoreLabel.Text", String.valueOf(newScore));
sendUpdate(builder);
}
// With event bindings update
private void addNewListItem(String itemName) {
UICommandBuilder commandBuilder = new UICommandBuilder();
UIEventBuilder eventBuilder = new UIEventBuilder();
int index = items.size();
String selector = "#ItemList[" + index + "]";
commandBuilder.append("#ItemList", "Components/ListItem.ui");
commandBuilder.set(selector + " #Name.Text", itemName);
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
selector,
EventData.of("Index", String.valueOf(index)),
false
);
sendUpdate(commandBuilder, eventBuilder, false);
}

When many things change, rebuild the entire page:

private void refreshPage() {
rebuild(); // Calls build() again and sends full update
}
// Clear list then repopulate
private void refreshList(List<String> items) {
UICommandBuilder builder = new UICommandBuilder();
UIEventBuilder eventBuilder = new UIEventBuilder();
builder.clear("#ItemList");
for (int i = 0; i < items.size(); i++) {
String selector = "#ItemList[" + i + "]";
builder.append("#ItemList", "Components/ListItem.ui");
builder.set(selector + " #Label.Text", items.get(i));
eventBuilder.addEventBinding(
CustomUIEventBindingType.Activating,
selector,
EventData.of("Index", String.valueOf(i))
);
}
sendUpdate(builder, eventBuilder, false);
}

Hytale uses .ui files as text-based assets that define UI layouts, styles, and components. These files are loaded by the client and referenced by server-side code via UICommandBuilder.

UI files are registered as text assets and can be edited in the asset editor:

assetTypeRegistry.registerAssetType(
new CommonAssetTypeHandler("UI", null, ".ui", AssetEditorEditorType.Text)
);

UI files follow an organized directory structure:

UI Assets
├── Common/
│ └── TextButton.ui # Reusable button component
├── Common.ui # Global styles and constants
└── Pages/
├── BasicTextButton.ui # Simple button templates
├── [Feature]Page.ui # Main page layouts
├── [Feature]*.ui # Related sub-components
└── [SubFeature]/ # Nested feature folders
└── *.ui

Based on decompiled code analysis, the following .ui files are referenced:

File PathDescription
Common.uiGlobal styles (DefaultTextButtonStyle, SecondaryTextButtonStyle)
Common/TextButton.uiReusable button with LabelStyle, SelectedLabelStyle
File PathPurpose
Pages/BasicTextButton.uiSimple text button template
Pages/DialogPage.uiDialog/conversation interface
Pages/ShopPage.uiShop interface layout
Pages/ShopItemButton.uiIndividual shop item button
Pages/BarterPage.uiBarter trading interface
Pages/BarterTradeRow.uiIndividual trade row element
Pages/GridLayoutSpacer.uiGrid layout spacing element
Pages/RespawnPage.uiDeath/respawn screen
Pages/DroppedItemElement.uiDropped item display
Pages/RespawnPointRenamePopup.uiRespawn point naming dialog
Pages/RespawnPointSelectPage.uiRespawn point selection
Pages/RespawnPointNearbyPage.uiNearby respawn points list
Pages/RespawnPointButton.uiRespawn button with styles
Pages/WarpListPage.uiWarp/teleport list
Pages/WarpListEntry.uiWarp list entry element
Pages/TeleporterSettingsPage.uiTeleporter settings
Pages/InstanceListPage.uiInstance management
Pages/ConfigureInstanceBlockPage.uiInstance block configuration
Pages/ChangeModelPage.uiModel selection interface
Pages/CommandListPage.uiCommand browser
Pages/SubCommand.uiSubcommand display
Pages/CommandVariant.uiCommand variant element
Pages/Parameter.uiCommand parameter element
Pages/ArgumentType.uiArgument type display
Pages/PluginListPage.uiPlugin browser
Pages/PluginListButton.uiPlugin list button
Pages/EntitySpawnPage.uiEntity spawner interface
Pages/ParticleSpawnPage.uiParticle spawner interface
Pages/PlaySoundPage.uiSound player interface
Pages/ChunkTintPage.uiChunk tinting settings
Pages/LaunchPadPage.uiLaunch pad configuration
Pages/PrefabSpawnerPage.uiPrefab spawner settings
Pages/ItemRepairPage.uiItem repair interface
Pages/ItemRepairElement.uiRepair item element
Pages/ObjectiveAdminPanelPage.uiObjective admin panel
Pages/ObjectiveDataSlot.uiObjective data slot
Pages/PortalDeviceSummonPage.uiPortal summoning interface
Pages/PortalDeviceActivePage.uiActive portal display
Pages/PortalDeviceErrorPage.uiPortal error display
Pages/PortalPillElement.uiPortal pill element
Pages/PortalBulletPoint.uiPortal bullet point
Pages/PrefabPage.uiPrefab browser
Pages/PrefabSavePage.uiPrefab save dialog
Pages/PrefabTeleportPage.uiPrefab teleporter
Pages/PrefabEditorLoadSettingsPage.uiPrefab editor settings
Pages/PrefabEditorSaveSettingsPage.uiPrefab save settings
Pages/PrefabEditorExitConfirmPage.uiExit confirmation
Pages/ObjImportPage.uiOBJ file import
Pages/ImageImportPage.uiImage import
Pages/ScriptedBrushPage.uiScripted brush list
Pages/MemoriesCategoryPanel.uiMemories category panel
Pages/MemoryCategory.uiMemory category element
Pages/MemoriesPanel.uiMemories panel
Pages/Memory.uiIndividual memory element
Pages/ChestMarkerElement.uiChest marker element

UI files follow consistent naming patterns:

PatternExampleUsage
[Feature]Page.uiShopPage.ui, DialogPage.uiMain page layouts
[Feature]Button.uiShopItemButton.ui, PluginListButton.uiButton templates
[Feature]Element.uiDroppedItemElement.ui, PortalPillElement.uiReusable elements
[Feature]Entry.uiWarpListEntry.uiList entry items
[Feature]Slot.uiObjectiveDataSlot.uiSlot elements
[Feature]Row.uiBarterTradeRow.uiRow layouts

UI elements are referenced using CSS-like selectors:

SyntaxDescriptionExample
#ElementIdSelect by ID#SaveButton
#ElementId.PropertyAccess property#Label.Text
#Parent[index]Array indexing#ItemList[0]
#Parent #ChildNested selection#Container #Title
#Parent #Child.PropertyNested property#List[0] #Name.Text

Use Value.ref() to reference styles defined in UI files:

// Reference a style from another UI file
Value<String> buttonStyle = Value.ref("Common.ui", "DefaultTextButtonStyle");
commandBuilder.set("#MyButton.Style", buttonStyle);
// Reference from component file
Value<String> labelStyle = Value.ref("Pages/BasicTextButton.ui", "SelectedLabelStyle");
commandBuilder.set("#Label.Style", labelStyle);
// Reference respawn button styles
Value<String> defaultStyle = Value.ref("Pages/RespawnPointButton.ui", "DefaultRespawnButtonStyle");
Value<String> selectedStyle = Value.ref("Pages/RespawnPointButton.ui", "SelectedRespawnButtonStyle");

UI files are loaded using UICommandBuilder:

UICommandBuilder builder = new UICommandBuilder();
// Load main page layout
builder.append("Pages/ShopPage.ui");
// Append child elements into containers
builder.append("#ItemContainer", "Pages/ShopItemButton.ui");
builder.append("#ItemContainer", "Pages/ShopItemButton.ui");
// Set properties on loaded elements
builder.set("#ItemContainer[0] #Name.Text", "Sword");
builder.set("#ItemContainer[0] #Price.Text", "100 gold");
builder.set("#ItemContainer[1] #Name.Text", "Shield");
builder.set("#ItemContainer[1] #Price.Text", "75 gold");

When creating custom pages, follow these conventions:

  1. Page files - Name as [Feature]Page.ui and place in Pages/
  2. Component files - Name descriptively and place in Common/ or alongside pages
  3. Define styles - Export reusable styles for server-side reference
  4. Use consistent IDs - Follow existing naming patterns for element IDs
  1. Use appropriate lifetime - Choose CantClose only when necessary (e.g., death screen)
  2. Don’t lock for real-time updates - Set locksInterface=false for sliders and search inputs
  3. Validate on server - Never trust client data; always validate in handleDataEvent
  4. Use translation keys - Use Message.translation() for localizable text
  5. Handle dismissal - Override onDismiss() for cleanup when pages are closed
  6. Use reference keys - Prefix with @ to read UI values at event time
  7. Batch updates - Combine multiple set() calls in one sendUpdate()
  8. Check validity - Verify entity references are valid before processing events
  9. Use suppliers - Implement CustomPageSupplier for interaction-triggered pages
  10. Define codecs properly - Create BuilderCodec for your event data classes
  11. Organize UI files - Keep page layouts in Pages/, shared components in Common/
  12. Reference styles - Use Value.ref() to reuse styles across UI files