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.

build return object

FieldTypeRequiredDescription
customIdstringYesMust be id().
valuesRecord<string, OptionDef | string>YesThe fixed option set. Keys become the selectable values.
placeholderstringNoPlaceholder text.
minValuesnumberNoMin selections. Default: 1.
maxValuesnumberNoMax selections. When 1, ctx.value is a single string. When > 1, ctx.values is an array.
disabledbooleanNoDisabled state.

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.

import { createTypedStringMenu } from "arcscord";

export const moodMenu = createTypedStringMenu({
route: "mood_menu",
build: id => ({
customId: id(),
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
placeholder: "How are you feeling?",
maxValues: 1,
}),
run: (ctx) => {
const mood = ctx.values; // typed as "great" | "okay" | "bad"
return ctx.reply(`Mood: ${mood}`);
},
});

as const on the inline 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.

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" })