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 |
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 = Observable.create<string>("Player", { clientWritable: true });
const difficulty = Observable.create<number>(1, { clientWritable: true });
const musicEnabled = Observable.create<boolean>(true, { clientWritable: true });
const volumeLevel = Observable.create<number>(75, { clientWritable: true });
CustomForm.create(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";
MessageBox.create(player)
.title("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) => {
if (response.selection === 0) {
deleteItem();
}
})
.catch(e => {
console.error(e);
});
Working with Observables
Creating Observables
// Read-only Observable (server controls the value)
const status = Observable.create<string>("Loading...");
// Client-writable Observable (UI controls can update it)
const userInput = Observable.create<string>("", { clientWritable: true });
Updating Observable values
const entityCount = Observable.create<number>(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 = Observable.create<number>(0, { clientWritable: true });
selectedOption.subscribe((newValue) => {
console.log(`Selection changed to: ${newValue}`);
updatePreview(newValue);
});
Using Observables for dynamic labels
const statusLabel = Observable.create<string>("Ready");
const itemCount = Observable.create<number>(0);
// Update label based on data changes
itemCount.subscribe((count) => {
statusLabel.setData(`Found ${count} items`);
});
CustomForm.create(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:
CustomForm.create(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) {
CustomForm.create(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 = Observable.create<string>("", { clientWritable: true });
const renderDistance = Observable.create<number>(12, { clientWritable: true });
const showParticles = Observable.create<boolean>(true, { clientWritable: true });
const language = Observable.create<number>(0, { clientWritable: true });
CustomForm.create(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 = Observable.create<string>("Scanning...");
const killButtonDisabled = Observable.create<boolean>(true);
const statusMessage = Observable.create<string>("");
// 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);
CustomForm.create(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() |
CustomForm.create(player, title) |
Player passed at creation |
new ModalFormData() |
CustomForm.create(player, title) |
Unified form type |
new MessageFormData() |
MessageBox.create(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 = Observable.create<number>(50, { clientWritable: true });
CustomForm.create(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 = CustomForm.create(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";
CustomForm.create(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 = Observable.create<boolean>(false, { clientWritable: true });
const isNotAdvancedMode = Observable.create<boolean>(true, { clientWritable: true });
const showAdvancedOptions = Observable.create<boolean>(false, { clientWritable: true });
isAdvancedMode.subscribe(newVal => {
isNotAdvancedMode.setData(!newVal);
});
CustomForm.create(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:
CustomForm.create(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);
});