Skip to main content

Select menus

Select menus are dropdown components. An action row can hold exactly one select menu. Arcscord supports all five Discord select menu types.

What they look like

String, user, mentionable and channel select menus in Discord
note

All select menu builder functions (stringSelectMenu, userSelectMenu, etc.) already return an ActionRowData — they wrap the component in an action row automatically. Pass the result directly to components: [...] without wrapping in actionRow().

Common options

These options apply to all select menu types.

OptionTypeRequiredDescription
customIdstringYesMust be set to id(). Max 100 chars.
placeholderstringNoGrey text shown when nothing is selected. Max 150 chars.
minValuesnumberNoMinimum number of selections required. Default: 1.
maxValuesnumberNoMaximum number of selections allowed. Default: 1.
disabledbooleanNoPrevents interaction. Default: false.

String select

Use createSelectMenu with ComponentType.StringSelect when you want to pass the option list at build time.

stringSelectMenu() options

OptionTypeRequiredDescription
optionsSelectOption[] | string[]YesThe selectable items. See below.
customIdstringYesCustom ID.
placeholderstringNoPlaceholder text.
minValuesnumberNoMin selections.
maxValuesnumberNoMax selections.
disabledbooleanNoDisabled state.

Each SelectOption object:

FieldTypeRequiredDescription
labelstringYesText shown in the dropdown. Max 100 chars.
valuestringYesValue received in ctx.values. Max 100 chars.
descriptionstringNoSubtext under the label. Max 100 chars.
emojiComponentEmojiResolvableNoEmoji displayed next to the label.
defaultbooleanNoPre-selects this option when the menu first renders.

Shorthand: pass a plain string to use it as both label and value.

When you pass options as variadic build arguments, each arg must be a plain string (the string becomes both label and value):

import { createSelectMenu, stringSelectMenu } from "arcscord";
import { ComponentType } from "discord.js";

export const categoryMenu = createSelectMenu({
type: ComponentType.StringSelect,
route: "category_menu",
build: (id, ...options) => stringSelectMenu({
customId: id(),
options,
placeholder: "Choose a category",
}),
run: ctx => ctx.reply(`Selected: ${ctx.values.join(", ")}`),
});

// send it — pass options as plain strings
categoryMenu.build("commands", "events", "components")

For rich options (label, description, emoji), define them directly inside build instead of using variadic args:

export const richCategoryMenu = createSelectMenu({
type: ComponentType.StringSelect,
route: "rich_category_menu",
build: id => stringSelectMenu({
customId: id(),
options: [
{ label: "Commands", value: "commands", description: "Slash commands & context menus" },
{ label: "Components", value: "components", description: "Buttons, selects, modals" },
{ label: "Events", value: "events" },
],
placeholder: "Choose a category",
}),
run: ctx => ctx.reply(`Selected: ${ctx.values.join(", ")}`),
});

richCategoryMenu.build() // no args needed

ctx.values is string[].

Typed string select

Use createTypedStringMenu when the option set is fixed at compile time. Arcscord infers the value type from the keys, so ctx.values is a typed union array.

Top-level options

FieldTypeRequiredDescription
valuesRecord<string, OptionDef | string>YesThe fixed option set. Keys become the selectable values. Captured immediately when createTypedStringMenu is called.
maxValuesnumberNoMax selections. When 1, ctx.value is a single string. When > 1, ctx.values is an array.
minValuesnumberNoMin selections. Default: 1.

Each option definition in values:

FieldTypeRequiredDescription
labelstringNoDisplayed label. Defaults to the key if omitted.
descriptionstringNoSubtext.
emojiComponentEmojiResolvableNoEmoji next to the label.
defaultbooleanNoPre-selects this option.

Or pass a plain string as the value — it becomes the label.

build return object

FieldTypeRequiredDescription
customIdstringYesMust be id().
placeholderstringNoPlaceholder text.
disabledbooleanNoDisabled state.
optionOverridesTypedSelectMenuOptionOverrides<typeof values>NoPresentation overrides keyed by the static option values.
import { createTypedStringMenu } from "arcscord";

export const moodMenu = createTypedStringMenu({
route: "mood_menu",
values: {
great: { label: "Great 🎉", description: "Feeling awesome" },
okay: { label: "Okay 😐" },
bad: { label: "Bad 😞", description: "Having a rough day" },
} as const, // as const is required for TypeScript to infer the key union
maxValues: 1,
build: id => ({
customId: id(),
placeholder: "How are you feeling?",
}),
run: (ctx) => {
const mood = ctx.values; // typed as "great" | "okay" | "bad"
return ctx.reply(`Mood: ${mood}`);
},
});

as const on the values object is what makes TypeScript narrow the type. Without it, ctx.values falls back to string.

When maxValues is 1, ctx.values is the single selected key (a string). When maxValues > 1, ctx.values is an array of keys.

values must be static

You must declare values as a static object literal asserted with as const, passed directly to createTypedStringMenu (not inside build). Its keys define both the ctx.values type and the set of values accepted at runtime — a selection outside that set (for example coming from an outdated message whose options no longer match) is rejected before run is called. Building values dynamically (or dropping as const) collapses ctx.values back to string and removes the type safety this helper provides.

Dynamic option presentation & i18next

The keys in values must stay static, but their presentation can vary for each call to build. Return optionOverrides from build to replace an option's label, description, emoji, or default state. Overrides are merged with the static definition, so omitted properties keep their original values.

optionOverrides is keyed by the keys of values. TypeScript rejects undeclared keys and does not offer a value property: changing presentation can therefore never change ctx.values typing or the runtime set used to validate interactions. The static object is not mutated, so the same handler can safely be built for several users or locales.

import { createTypedStringMenu } from "arcscord";

const values = {
great: { label: "Great", description: "Feeling awesome", emoji: "🎉" },
okay: { label: "Okay", description: "Doing fine" },
bad: { label: "Bad", description: "Having a rough day" },
} as const;

type MoodPresentation = {
placeholder: string;
great: string;
greatDescription: string;
okay: string;
bad: string;
defaultMood: "great" | "okay" | "bad";
};

export const localizedMoodMenu = createTypedStringMenu({
route: "localized_mood",
values,
maxValues: 1,
build: (id, presentation: MoodPresentation) => ({
customId: id(),
placeholder: presentation.placeholder,
optionOverrides: {
great: {
label: presentation.great,
description: presentation.greatDescription,
emoji: "✨",
default: presentation.defaultMood === "great",
},
okay: {
label: presentation.okay,
default: presentation.defaultMood === "okay",
},
bad: {
label: presentation.bad,
default: presentation.defaultMood === "bad",
},
},
}),
run: async ctx => ctx.reply(`Mood: ${ctx.values}`),
});

Resolve translations where ctx.t is available, then pass one object to build, just as with a typed modal:

localizedMoodMenu.build({
placeholder: ctx.t($ => $.mood.placeholder),
great: ctx.t($ => $.mood.great.label),
greatDescription: ctx.t($ => $.mood.great.description),
okay: ctx.t($ => $.mood.okay.label),
bad: ctx.t($ => $.mood.bad.label),
defaultMood: "okay",
})

User select

Lets the user pick one or more server members.

Additional options

OptionTypeDescription
defaultValuesArray<{ id: string; type: "user" }>Pre-selected users shown when the menu renders.
import { createSelectMenu, userSelectMenu } from "arcscord";
import { ComponentType } from "discord.js";

export const assignMenu = createSelectMenu({
type: ComponentType.UserSelect,
route: "assign_user",
build: (id) => userSelectMenu({
customId: id(),
placeholder: "Assign to…",
maxValues: 3,
}),
run: (ctx) => {
const names = ctx.values.map(u => u.username).join(", ");
return ctx.reply(`Assigned to: ${names}`);
},
});

ctx.values is User[].

Role select

Lets the user pick one or more roles.

Additional options

OptionTypeDescription
defaultValuesArray<{ id: string; type: "role" }>Pre-selected roles.
export const roleMenu = createSelectMenu({
type: ComponentType.RoleSelect,
route: "role_picker",
build: (id, placeholder: string) => roleSelectMenu({
customId: id(),
placeholder,
}),
run: ctx => ctx.reply(`Role: ${ctx.values[0]?.name}`),
});

ctx.values is Role[].

Mentionable select

Lets the user pick users or roles in the same menu.

Additional options

OptionTypeDescription
defaultValuesArray<{ id: string; type: "user" | "role" }>Pre-selected users or roles.
export const targetMenu = createSelectMenu({
type: ComponentType.MentionableSelect,
route: "target_picker",
build: (id) => mentionableSelectMenu({ customId: id(), placeholder: "Mention a user or role" }),
run: (ctx) => {
const name = ctx.values.map(v => "username" in v ? v.username : v.name).join(", ");
return ctx.reply(`Target: ${name}`);
},
});

ctx.values is (User | Role)[].

Channel select

Lets the user pick one or more channels. Optionally filter by channel type.

Additional options

OptionTypeDescription
channelTypesChannelType[]Restrict which channel types appear.
defaultValuesArray<{ id: string; type: "channel" }>Pre-selected channels.

Available channelTypes values:

ValueDescription
"guildText"Standard text channel
"guildVoice"Voice channel
"guildCategory"Category
"guildAnnouncement"Announcement channel
"publicThread"Public thread
"privateThread"Private thread
"announcementThread"Thread inside announcement channel
"guildStageVoice"Stage channel
"guildForum"Forum channel
"guildMedia"Media channel
"dm"Direct message
"groupDm"Group DM
export const channelMenu = createSelectMenu({
type: ComponentType.ChannelSelect,
route: "channel_picker",
build: (id) => channelSelectMenu({
customId: id(),
placeholder: "Pick a channel",
channelTypes: ["guildText", "guildAnnouncement"], // text-like only
}),
run: ctx => ctx.reply(`Channel: #${ctx.values[0]?.id}`),
});

ctx.values is Channel[].

createSelectMenu() handler options

These apply to all select menu handler types.

OptionTypeRequiredDescription
typeComponentTypeYesThe menu type (StringSelect, UserSelect, RoleSelect, MentionableSelect, ChannelSelect).
routestringYesCustom ID pattern, same rules as buttons.
build(id, ...args) => ActionRowYesReturns the built select menu wrapped in an action row.
run(ctx) => ResultYesInteraction handler. ctx.values type depends on the menu type.
preReplytrue | "ephemeral"NoDefer before middlewares.
useComponentMiddleware[]NoMiddleware chain.

Route parameters

Select menus support the same {paramName} route pattern as buttons. Pass params as an object to .build()id() always takes no arguments inside build:

export const ticketAssign = createSelectMenu({
type: ComponentType.UserSelect,
route: "ticket/{ticketId}/assign",
build: id => userSelectMenu({
customId: id(),
placeholder: "Assign to…",
}),
run: ctx => ctx.reply(`Assigned ticket ${ctx.params.ticketId} to ${ctx.values[0]?.username}`),
});

// sending it
ticketAssign.build({ ticketId: "42" })