Button Variants
Button Variants
Button is a sealed interface with six built-in variants. You do not implement it
yourself. You choose the variant that matches the behavior you want.
| Variant | Use it for |
|---|---|
StaticButton |
Decoration or labels |
ClickableButton |
Normal clickable actions |
TransformingButton |
Replace this slot after a click |
SpanningButton |
One logical button across multiple slots |
CompositeButton |
One click triggering several child actions |
CycleButton |
Repeating state changes like toggles |
All buttons expose:
item()data()withItem(...)dispatch(...)
1. StaticButton
Button filler = Button.of(new ItemStack(Material.PAPER));
This is the simplest option. It shows an item and does nothing on click.
Use it for:
- borders
- separators
- titles
- blank filler slots
2. ClickableButton
Button buyButton = Button.clickable(
new ItemStack(Material.DIAMOND),
(view, event) -> view.viewer().sendMessage("Bought!")
);
This is the default choice for most real menu actions.
ClickAction also composes:
ClickAction playSound = (view, event) -> playShopSound(view.viewer());
ClickAction sendMessage = (view, event) -> view.viewer().sendMessage("Bought!");
ClickAction action = playSound.andThen(sendMessage);
Button buyButton = Button.clickable(new ItemStack(Material.DIAMOND), action);
3. TransformingButton
Use this when a slot should change into a different button after being clicked.
Button toggle = Button.transforming(
new ItemStack(Material.REDSTONE_LAMP),
(view, event) -> Button.of(new ItemStack(Material.GLOWSTONE))
);
Returning null leaves the slot unchanged.
TransformingButton is ideal for one-way or one-step changes.
If the slot should keep cycling through several states, use CycleButton instead.
4. SpanningButton
Use this when several slots should behave like one large button.
SpanningButton banner = Button.spanning(
new ItemStack(Material.WOOL),
Set.of(Slot.of(10), Slot.of(11), Slot.of(12)),
(view, event) -> view.viewer().sendMessage("Banner clicked")
);
content.placeSpanning(banner);
Every slot in the footprint points to the same button instance.
To remove a spanning button cleanly, clear every slot in its footprint. Removing only one slot leaves the rest visible.
5. CompositeButton
Use this when one click should run several child buttons in order.
ItemStack icon = new ItemStack(Material.EMERALD);
Button button = Button.composite(
icon,
Button.clickable(icon, (view, event) -> playShopSound(view.viewer())),
Button.clickable(icon, (view, event) -> view.viewer().sendMessage("Purchased")),
Button.clickable(icon, (view, event) -> grantReward(view.viewer()))
);
The composite's own item() is what gets rendered. Child items are ignored visually.
Avoid putting TransformingButton or CycleButton inside a CompositeButton. Mutating children
change the clicked slot while later children are still trying to run.
6. CycleButton
Use this for repeated state progression:
Button filterButton = Button.cycle(
Button.clickable(offIcon(), (view, event) -> setFilter(view, "OFF")),
Button.clickable(whitelistIcon(), (view, event) -> setFilter(view, "WHITELIST")),
Button.clickable(blacklistIcon(), (view, event) -> setFilter(view, "BLACKLIST"))
);
What happens on click:
- the current state's own action runs
- Lotus creates the next
CycleButton - Lotus writes that new button back into the slot
CycleButton is immutable. The slot advances by replacing itself with a new cycle instance.
Attaching Data To A Button
Each button carries its own DataRegistry, which is perfect for storing model IDs:
Key<UUID> TARGET = Key.of("target", UUID.class);
ClickableButton playerButton = Button.clickable(icon, (view, event) -> {
view.content().get(Slot.of(event.getSlot()))
.flatMap(button -> button.data().get(TARGET))
.ifPresent(this::inspectPlayer);
});
playerButton.data().put(TARGET, somePlayer.getUniqueId());
This pattern becomes especially useful in pagination.
Next: Content Builder.