Hyperion

Buttons

A button handler runs when someone clicks a button your bot sent. Handlers don't remember anything between clicks, so if a button needs data (like which ticket it closes), put it in the custom ID or your own storage.

A button handler

Put handlers in src/buttons:

// src/buttons/refresh-profile.ts
import { PrimaryButtonBuilder } from "@discordjs/builders";
import { defineButton } from "@hyperion-js/core";

export default defineButton({
    builder: new PrimaryButtonBuilder()
        .setCustomId("profile.refresh")
        .setLabel("Refresh profile"),
    run: async ctx => {
        await ctx.interaction.update({ content: "Refreshed!" });
    },
});

The builder is the handler's template: the button your commands send copies of. Its custom ID is the one the handler answers to, and it has to match exactly. The handler takes these options:

OptionWhat it's for
builderThe template. Its custom ID becomes the handler's.
customIdThe custom ID to answer, when you'd rather not keep a template.
paramsData in the custom ID instead of a fixed one. See data in custom IDs.
guardsChecks before run(). See guards.
runRuns on each click.

Give the custom ID in one place only: TypeScript rejects a handler with both builder and customId. Link and premium buttons never reach your bot, so they can't be templates.

Sending the button

In a command, build() gives you a copy of the template:

await ctx.interaction.reply({
    content: "Press refresh to load your profile.",
    components: [
        new ActionRowBuilder().addComponents(
            ctx.client.buttons.build("profile.refresh"),
        ),
    ],
});

Every call returns a fresh copy, so change its label or disable it without affecting anyone else. It throws if the handler isn't loaded or has no template, which catches typos early.

You don't have to use templates at all. Build the button yourself and give the handler a customId; the handler doesn't care what the button looks like.

Changing the button after a click

Handlers are shared by everyone, so build a fresh copy rather than editing the template. A handler can refer to itself through its own constant:

const refresh = defineButton({
    builder: refreshButton,
    run: async ctx => {
        const button = ctx.client.buttons
            .build(refresh.customId)
            .setLabel("Refreshed");

        await ctx.interaction.update({
            components: [new ActionRowBuilder().addComponents(button)],
        });
    },
});

export default refresh;

Many buttons, one handler

Need a Close button on every ticket? Instead of a handler per ticket, give one handler params and put the ticket's ID in each button's custom ID. See data in custom IDs.

Guards

A command's guards don't carry over to the buttons it sends, so protect the button itself:

defineButton({
    builder: refreshButton,
    guards: [ownerOnly],
    run: async () => {},
});

For what happens when a click fails, see responding.

On this page