Hyperion

Select menus

Select menus work just like buttons, with one extra option: which kind of menu the handler answers. If you need a refresher on the kinds themselves, see the discord.js guide on select menus.

A string menu

Put handlers in src/select-menus:

// src/select-menus/choose-colour.ts
import { StringSelectMenuBuilder } from "@discordjs/builders";
import { defineSelectMenu } from "@hyperion-js/core";
import { ComponentType } from "discord.js";

export default defineSelectMenu({
    componentType: ComponentType.StringSelect,
    builder: new StringSelectMenuBuilder()
        .setCustomId("colour.choose")
        .setPlaceholder("Choose a colour")
        .addOptions(
            { label: "Blue", value: "blue" },
            { label: "Green", value: "green" },
        ),
    run: async ctx => {
        await ctx.interaction.update({
            content: `You chose ${ctx.interaction.values[0]}.`,
        });
    },
});

A select-menu handler takes the same options as a button handler, plus two:

OptionWhat it's for
componentTypeThe kind of menu. Required.
builderThe template. Its custom ID becomes the handler's.
customIdThe custom ID to answer, when there's no template.
paramsData in the custom ID. See data in custom IDs.
inputA schema for the picked values. See checking the pick.
guardsChecks before run(). See guards.
runRuns on each click.

All five kinds share one registry, so custom IDs must be unique across them.

The other kinds

For a user, role, mentionable, or channel menu, set componentType to that kind. It types ctx.interaction for the menu, so the picked users are right there:

export default defineSelectMenu({
    componentType: ComponentType.UserSelect,
    builder: new UserSelectMenuBuilder()
        .setCustomId("user.choose")
        .setPlaceholder("Choose a user"),
    run: async ctx => {
        const names = ctx.interaction.users.map(user => user.username);
        await ctx.interaction.update({
            content: `Selected: ${names.join(", ")}`,
        });
    },
});

Sending a menu

build() copies the template, the same as for buttons:

await ctx.interaction.reply({
    content: "Pick a colour.",
    components: [
        new ActionRowBuilder().addComponents(
            ctx.client.selectMenus.build("colour.choose"),
        ),
    ],
});

Checking the pick

Values come from the user's client, so an old message or a tampered request can send something your menu never offered. Give the handler input to check them first, the same way as command input:

const input = defineInput({
    // The schema gets the picked values, as strings.
    schema: v.pipe(v.array(v.picklist(["blue", "green"])), v.length(1)),
});

export default defineSelectMenu({
    customId: "colour.choose",
    componentType: ComponentType.StringSelect,
    input,
    run: async ctx => {
        const [issues, colours] = ctx.input;

        if (issues) {
            await ctx.interaction.reply({
                content: "That colour isn't on the list.",
                flags: MessageFlags.Ephemeral,
            });

            return;
        }

        await ctx.interaction.update({ content: `You chose ${colours[0]}.` });
    },
});

For user, role, mentionable, and channel menus, the schema gets the picked IDs.

A menu can carry data in its custom ID too, for example which ticket an "Assign to…" menu belongs to. The data arrives in ctx.params, separately from what the user picked. See data in custom IDs.

Guards

Guards work exactly as for buttons.

On this page