Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The Data-Driven UI (DDUI) framework is a modern approach to building user interfaces in Minecraft Bedrock Edition scripts. It enables dynamic, real-time UI updates without rebuilding forms. New modal forms is the first step in our UI journey for creators. Stay tuned for more features in future updates!
This guide introduces the DDUI framework and highlights the key differences from the existing @minecraft/server-ui module.
Summary
The DDUI framework provides a modern, reactive approach to building user interfaces in Minecraft Bedrock scripts. Key advantages include:
- Reactive data binding: UI automatically updates when Observable values change
- Inline callbacks: Cleaner code with callbacks directly on buttons
- Dynamic controls: Show, hide, enable, and disable controls at runtime
- Unified form type:
CustomFormhandles most UI scenarios
For API reference documentation, see the @minecraft/server-ui module reference.
Prerequisites
- Basic knowledge of TypeScript and Minecraft scripting
- A behavior pack with scripting enabled
Overview of the frameworks
Existing server-ui module
The existing APIs provide three main form types:
| Class | Purpose |
|---|---|
ActionFormData |
Displays a list of buttons for player selection |
MessageFormData |
Shows a simple two-button confirmation dialog |
ModalFormData |
Presents a questionnaire with various input controls |
These forms are static—once displayed, their content cannot change until the player closes and reopens them.
DDUI framework
The DDUI framework introduces:
| Class | Purpose |
|---|---|
CustomForm |
A unified, flexible form with dynamic content and reactive bindings |
MessageBox |
A streamlined two-button dialog with enhanced options |
Observable* |
A reactive wrapper for values that automatically updates the UI where * is String, Boolean, Number, and UIRawMessage |
DDUI forms are dynamic—content can update in real-time while the form is open.
Form types
CustomForm
CustomForm is the primary form type in DDUI. It combines the functionality of all three server-ui form types into a single, flexible class.
import { CustomForm, Observable } from "@minecraft/server-ui";
const playerName = new ObservableString("Player", { clientWritable: true });
const difficulty = new ObservableNumber(1, { clientWritable: true });
const musicEnabled = new ObservableBoolean(true, { clientWritable: true });
const volumeLevel = new ObservableNumber(75, { clientWritable: true });
new CustomForm(player, "Game Settings")
.closeButton()
.spacer()
.label("General Settings")
.divider()
.textField("Player Name", playerName, {
description: "Your display name in-game"
})
.spacer()
.label("Difficulty")
.dropdown("", difficulty, [
{ label: "Peaceful", value: 0 },
{ label: "Easy", value: 1 },
{ label: "Normal", value: 2 },
{ label: "Hard", value: 3 }
])
.spacer()
.divider()
.label("Audio Settings")
.toggle("Music Enabled", musicEnabled)
.slider("Volume", volumeLevel, 0, 100, {
description: "Master volume level",
step: 5
})
.spacer()
.button("Reset to Defaults", () => resetSettings())
.show()
.then(() => {
console.log(`Settings saved: ${playerName.getData()}`);
})
.catch(e => {
console.error(e);
});
MessageBox
MessageBox provides a streamlined way to create simple two-button dialogs:
import { MessageBox } from "@minecraft/server-ui";
new MessageBox(player, "Confirm Action")
.body("Are you sure you want to delete this item? This action cannot be undone.")
.button1("Delete")
.button2("Cancel", "Keep the item and close this dialog")
.show()
.then((response) => {
// The selection will be undefined if they user did not make a selection
// and the UI was closed, 1 for button 1 and 2 for button 2.
if (response.selection === 1) {
deleteItem();
}
})
.catch(e => {
console.error(e);
});
Working with Observables
Creating Observables
// Read-only Observable (server controls the value)
const status = new ObservableString("Loading...");
// Client-writable Observable (UI controls can update it)
const userInput = new ObservableString("", { clientWritable: true });
Updating Observable values
const entityCount = new ObservableNumber(0);
// Update the value - UI automatically reflects the change
system.runInterval(() => {
const entities = dimension.getEntities();
entityCount.setData(entities.length);
}, 20);
Subscribing to changes
const selectedOption = new ObservableNumber(0, { clientWritable: true });
selectedOption.subscribe((newValue) => {
console.log(`Selection changed to: ${newValue}`);
updatePreview(newValue);
});
Using Observables for dynamic labels
const statusLabel = new ObservableString("Ready");
const itemCount = new ObservableNumber(0);
// Update label based on data changes
itemCount.subscribe((count) => {
statusLabel.setData(`Found ${count} items`);
});
new CustomForm(player, "Inventory")
.label(statusLabel) // Label updates automatically
.button("Refresh", () => refreshInventory())
.show()
.catch(e => {
console.error(e);
});
Localization support
DDUI supports localized text using the RawMessage format:
new CustomForm(player, { translate: "ui.settings.title" })
.label({ translate: "ui.settings.description", with: ["value1", "value2"] })
.button({ translate: "ui.button.save" }, () => save())
.show()
.catch(e => {
console.error(e);
});
Examples
Example 1: Simple action menu
import { CustomForm } from "@minecraft/server-ui";
function showMenu(player: Player) {
new CustomForm(player, "Main Menu")
.label("What would you like to do?")
.spacer()
.button("Play Game", () => startGame(player))
.button("Settings", () => openSettings(player))
.button("Exit", () => exitGame(player))
.show()
.catch(e => {
console.error(e);
});
}
Example 2: Settings form with validation
import { CustomForm, Observable } from "@minecraft/server-ui";
function showSettings(player: Player) {
const username = new ObservableString("", { clientWritable: true });
const renderDistance = new ObservableNumber(12, { clientWritable: true });
const showParticles = new ObservableBoolean(true, { clientWritable: true });
const language = new ObservableNumber(0, { clientWritable: true });
new CustomForm(player, "Settings")
.textField("Username", username, {
description: "Your display name"
})
.slider("Render Distance", renderDistance, 2, 32, {
step: 1,
description: "Higher values may impact performance"
})
.toggle("Show Particles", showParticles, {
description: "Display particle effects"
})
.dropdown("Language", language, [
{ label: "English", value: 0 },
{ label: "Spanish", value: 1 },
{ label: "French", value: 2 }
])
.closeButton()
.show()
.then(() => {
applySettings(
username.getData(),
renderDistance.getData(),
showParticles.getData(),
language.getData()
);
})
.catch(e => {
console.error(e);
});
}
Example 3: Real-time updating form
This example demonstrates a capability unique to DDUI—updating form content while it's open:
import { CustomForm, Observable } from "@minecraft/server-ui";
import { system, TicksPerSecond } from "@minecraft/server";
function showEntityMonitor(player: Player) {
const entityStatus = new ObservableString("Scanning...");
const killButtonDisabled = new ObservableBoolean(true);
const statusMessage = new ObservableString("");
// Update entity count every 4 seconds while form is open
const intervalId = system.runInterval(() => {
const nearbyEntities = player.dimension.getEntities({
location: player.location,
maxDistance: 20
});
entityStatus.setData(`Found ${nearbyEntities.length} entities within 20 blocks`);
killButtonDisabled.setData(nearbyEntities.length === 0);
}, TicksPerSecond * 4);
new CustomForm(player, "Entity Monitor")
.spacer()
.label(entityStatus)
.spacer()
.button("Remove All Entities", () => {
const entities = player.dimension.getEntities({
location: player.location,
maxDistance: 20,
excludeTypes: ["minecraft:player"]
});
let count = 0;
for (const entity of entities) {
entity.kill();
count++;
}
statusMessage.setData(`Removed ${count} entities`);
}, {
tooltip: "Removes all non-player entities within range",
disabled: killButtonDisabled
})
.spacer()
.divider()
.label(statusMessage)
.closeButton()
.show()
.then(() => {
// Clean up interval when form closes
system.clearRun(intervalId);
})
.catch(e => {
console.error(e);
});
}
Migration guide
When migrating from @minecraft/server-ui to DDUI:
| Server-ui | DDUI | Notes |
|---|---|---|
new ActionFormData() |
new CustomForm(player, title) |
Player passed at creation |
new ModalFormData() |
new CustomForm(player, title) |
Unified form type |
new MessageFormData() |
new MessageBox(player) |
Simplified message dialogs |
.show(player) |
.show() |
Player already provided |
.button("text") |
.button("text", callback) |
Inline callbacks |
response.formValues[0] |
observable.getData() |
Direct value access |
| Static content only | Dynamic with Observable | Real-time updates |
Key differences
1. Reactive data binding with Observable
The most significant difference is the introduction of the Observable class. Instead of passing static values to form controls, you pass reactive Observable wrappers that can update the UI automatically.
Server-ui (static)
import { ModalFormData } from "@minecraft/server-ui";
const form = new ModalFormData()
.title('Settings')
.slider('Volume', 0, 100, { valueStep: 5, defaultValue: 50 }); // Static default of 50
form.show(player).then((response) => {
if (!response.canceled) {
const volume = response.formValues[0]; // Access by index
}
})
.catch(e => {
console.error(e);
});
DDUI (reactive)
import { CustomForm, Observable } from "@minecraft/server-ui";
const volume = new ObservableNumber(50, { clientWritable: true });
new CustomForm(player, 'Settings')
.slider('Volume', volume, 0, 100, { step: 5 })
.show()
.then(() => {
const currentVolume = volume.getData(); // Access via Observable
})
.catch(e => {
console.error(e);
});
Tip
The clientWritable: true option allows the client to update the Observable value when the player interacts with the control.
2. Form creation pattern
Server-ui
Forms are created with a constructor and the player is passed to show():
const form = new ActionFormData()
.title("My Form")
.body("Select an option")
.button("Option 1");
form.show(player)
.catch(e => {
console.error(e);
});
DDUI
Forms are created with a static factory method that accepts both the player and title:
const form = new CustomForm(player, "My Form")
.label("Select an option")
.button("Option 1", () => handleOption1());
form.show()
.catch(e => {
console.error(e);
});
3. Button callbacks vs. selection indices
Server-ui
Button selection is determined by checking an index in the response:
import { ActionFormData } from "@minecraft/server-ui";
new ActionFormData()
.title("Actions")
.button("Save")
.button("Load")
.button("Exit")
.show(player)
.then((response) => {
if (response.canceled) return;
switch (response.selection) {
case 0: save(); break;
case 1: load(); break;
case 2: exit(); break;
}
})
.catch(e => {
console.error(e);
});
DDUI
Buttons accept inline callback functions that execute when pressed:
import { CustomForm } from "@minecraft/server-ui";
new CustomForm(player, "Actions")
.button("Save", () => save())
.button("Load", () => load())
.button("Exit", () => exit())
.show()
.catch(e => {
console.error(e);
});
Note
DDUI callbacks execute immediately when the button is pressed, while the form remains open. This enables multi-action forms where users can interact multiple times before closing.
4. Dynamic form controls
DDUI forms can dynamically enable, disable, show, or hide controls based on Observable values.
const isAdvancedMode = new ObservableBoolean(false, { clientWritable: true });
const isNotAdvancedMode = new ObservableBoolean(true, { clientWritable: true });
const showAdvancedOptions = new ObservableBoolean(false, { clientWritable: true });
isAdvancedMode.subscribe(newVal => {
isNotAdvancedMode.setData(!newVal);
});
new CustomForm(player, "Settings")
.toggle("Enable Advanced Mode", isAdvancedMode)
.toggle("Show Advanced Options", showAdvancedOptions)
.button("Advanced Settings", () => openAdvancedSettings(), {
visible: showAdvancedOptions,
disabled: isNotAdvancedMode // Disabled when advanced mode is off
})
.show()
.catch(e => {
console.error(e);
});
5. Enhanced control options
DDUI controls support additional options like descriptions and tooltips:
new CustomForm(player, "Player Settings")
.textField("Display Name", playerName, {
description: "Enter the name shown to other players",
disabled: isLocked
})
.button("Apply Changes", () => applyChanges(), {
tooltip: "Save your current settings"
})
.dropdown("Difficulty", selectedDifficulty, [
{ label: "Easy", value: 0, description: "Reduced enemy damage" },
{ label: "Normal", value: 1, description: "Standard gameplay" },
{ label: "Hard", value: 2, description: "Increased challenge" }
])
.show()
.catch(e => {
console.error(e);
});
6. Dealing with User Busy
DDUI forms will not show if another UI is open. You can check for this by looking at the result of the show methods:
new CustomForm(player, "Player Settings")
.textField("Display Name", playerName, {
description: "Enter the name shown to other players",
disabled: isLocked
})
.show()
.then(showResult => {
if (showResult === DataDrivenScreenClosedReason.UserBusy) {
player.sendMessage("Player was busy, try again.");
return;
}
player.sendMessage(`Display name is ${playerName.getData()}`)
})
.catch(e => {
console.error(e);
});