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:

  1. the current state's own action runs
  2. Lotus creates the next CycleButton
  3. 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.