Comprehensive message components system including action rows, buttons, select menus, modals, and text inputs with builder patterns for creating interactive Discord interfaces.
Container component that holds other interactive components in a horizontal row.
/**
* Builder for creating action rows
* @template ComponentType The type of components this action row can contain
*/
class ActionRowBuilder<ComponentType extends AnyComponentBuilder = AnyComponentBuilder> {
constructor(data?: Partial<ActionRowData<ActionRowComponentData | JSONEncodable<APIComponentInActionRow>>>);
// Component management
addComponents(...components: ComponentType[]): this;
setComponents(...components: ComponentType[]): this;
spliceComponents(index: number, deleteCount: number, ...components: ComponentType[]): this;
// Utility methods
toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
static from<T extends AnyComponentBuilder = AnyComponentBuilder>(
other: JSONEncodable<APIActionRowComponent<ReturnType<T['toJSON']>>> | APIActionRowComponent<ReturnType<T['toJSON']>>
): ActionRowBuilder<T>;
}
/**
* Represents an action row component
* @template ComponentType The type of components this action row contains
*/
class ActionRow<ComponentType extends MessageActionRowComponent | ModalActionRowComponent> {
readonly type: ComponentType.ActionRow;
readonly components: ComponentType[];
toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
}
interface ActionRowData<ComponentType extends JSONEncodable<APIComponentInActionRow> | ActionRowComponentData> {
type: ComponentType.ActionRow;
components: readonly ComponentType[];
}
type MessageActionRowComponent =
| ButtonComponent
| StringSelectMenuComponent
| UserSelectMenuComponent
| RoleSelectMenuComponent
| MentionableSelectMenuComponent
| ChannelSelectMenuComponent;
type ModalActionRowComponent = TextInputComponent;
type MessageActionRowComponentBuilder =
| ButtonBuilder
| StringSelectMenuBuilder
| UserSelectMenuBuilder
| RoleSelectMenuBuilder
| MentionableSelectMenuBuilder
| ChannelSelectMenuBuilder;
type ModalActionRowComponentBuilder = TextInputBuilder;Interactive buttons for message interfaces with various styles and states.
/**
* Builder for creating button components
*/
class ButtonBuilder {
constructor(data?: Partial<ButtonComponentData | APIButtonComponent>);
// Button configuration
setCustomId(customId: string): this;
setLabel(label: string): this;
setStyle(style: ButtonStyle): this;
setEmoji(emoji: ComponentEmojiResolvable): this;
setURL(url: string): this;
setDisabled(disabled?: boolean): this;
// Utility methods
toJSON(): APIButtonComponent;
static from(other: JSONEncodable<APIButtonComponent> | APIButtonComponent): ButtonBuilder;
}
/**
* Represents a button component
*/
class ButtonComponent extends Component<APIButtonComponent> {
readonly type: ComponentType.Button;
readonly style: ButtonStyle;
readonly label: string | null;
readonly emoji: ComponentEmoji | null;
readonly disabled: boolean;
readonly customId: string | null;
readonly url: string | null;
toJSON(): APIButtonComponent;
}
const ButtonStyle: {
Primary: 1; // Blurple button
Secondary: 2; // Grey button
Success: 3; // Green button
Danger: 4; // Red button
Link: 5; // Link button (requires URL)
};
interface ButtonComponentData extends BaseComponentData {
type: ComponentType.Button;
style: ButtonStyle;
label?: string;
emoji?: ComponentEmojiResolvable;
custom_id?: string;
url?: string;
disabled?: boolean;
}
interface ComponentEmoji {
id: Snowflake | null;
name: string | null;
animated?: boolean;
}
type ComponentEmojiResolvable =
| string
| ComponentEmoji
| EmojiIdentifierResolvable;Usage Examples:
// Basic buttons
const primaryButton = new ButtonBuilder()
.setCustomId('primary_action')
.setLabel('Primary Action')
.setStyle(ButtonStyle.Primary);
const secondaryButton = new ButtonBuilder()
.setCustomId('secondary_action')
.setLabel('Secondary')
.setStyle(ButtonStyle.Secondary)
.setEmoji('βοΈ');
const linkButton = new ButtonBuilder()
.setLabel('Visit Website')
.setStyle(ButtonStyle.Link)
.setURL('https://discord.js.org');
// Action row with buttons
const buttonRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents(primaryButton, secondaryButton, linkButton);
await interaction.reply({
content: 'Choose an action:',
components: [buttonRow]
});
// Disabled button
const disabledButton = new ButtonBuilder()
.setCustomId('disabled_action')
.setLabel('Disabled')
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);Dropdown menus for user selection with different target types.
/**
* Base class for select menu builders
*/
class SelectMenuBuilder {
constructor(data?: Partial<SelectMenuComponentData>);
// Menu configuration
setCustomId(customId: string): this;
setPlaceholder(placeholder: string): this;
setMinValues(minValues: number): this;
setMaxValues(maxValues: number): this;
setDisabled(disabled?: boolean): this;
toJSON(): APISelectMenuComponent;
}
/**
* Builder for string select menus
*/
class StringSelectMenuBuilder extends SelectMenuBuilder {
// Option management
addOptions(...options: (StringSelectMenuOptionBuilder | APISelectMenuOption)[]): this;
setOptions(...options: (StringSelectMenuOptionBuilder | APISelectMenuOption)[]): this;
spliceOptions(index: number, deleteCount: number, ...options: (StringSelectMenuOptionBuilder | APISelectMenuOption)[]): this;
toJSON(): APIStringSelectComponent;
}
/**
* Builder for user select menus
*/
class UserSelectMenuBuilder extends SelectMenuBuilder {
// Default values for user selection
setDefaultUsers(...users: UserResolvable[]): this;
addDefaultUsers(...users: UserResolvable[]): this;
toJSON(): APIUserSelectComponent;
}
/**
* Builder for role select menus
*/
class RoleSelectMenuBuilder extends SelectMenuBuilder {
setDefaultRoles(...roles: RoleResolvable[]): this;
addDefaultRoles(...roles: RoleResolvable[]): this;
toJSON(): APIRoleSelectComponent;
}
/**
* Builder for channel select menus
*/
class ChannelSelectMenuBuilder extends SelectMenuBuilder {
// Channel type filtering
setChannelTypes(...channelTypes: RestOrArray<ChannelType>): this;
addChannelTypes(...channelTypes: RestOrArray<ChannelType>): this;
setDefaultChannels(...channels: ChannelResolvable[]): this;
addDefaultChannels(...channels: ChannelResolvable[]): this;
toJSON(): APIChannelSelectComponent;
}
/**
* Builder for mentionable select menus (users and roles)
*/
class MentionableSelectMenuBuilder extends SelectMenuBuilder {
setDefaultUsers(...users: UserResolvable[]): this;
addDefaultUsers(...users: UserResolvable[]): this;
setDefaultRoles(...roles: RoleResolvable[]): this;
addDefaultRoles(...roles: RoleResolvable[]): this;
toJSON(): APIMentionableSelectComponent;
}
/**
* Builder for select menu options (string select only)
*/
class StringSelectMenuOptionBuilder {
constructor(data?: Partial<APISelectMenuOption>);
setLabel(label: string): this;
setValue(value: string): this;
setDescription(description: string): this;
setEmoji(emoji: ComponentEmojiResolvable): this;
setDefault(isDefault?: boolean): this;
toJSON(): APISelectMenuOption;
static from(other: JSONEncodable<APISelectMenuOption> | APISelectMenuOption): StringSelectMenuOptionBuilder;
}Usage Examples:
// String select menu with options
const stringSelect = new StringSelectMenuBuilder()
.setCustomId('music_genre')
.setPlaceholder('Choose a music genre')
.setMinValues(1)
.setMaxValues(3)
.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel('Rock')
.setValue('rock')
.setDescription('Rock music')
.setEmoji('πΈ'),
new StringSelectMenuOptionBuilder()
.setLabel('Jazz')
.setValue('jazz')
.setDescription('Jazz music')
.setEmoji('π·'),
new StringSelectMenuOptionBuilder()
.setLabel('Classical')
.setValue('classical')
.setDescription('Classical music')
.setEmoji('π»')
);
// User select menu
const userSelect = new UserSelectMenuBuilder()
.setCustomId('select_moderators')
.setPlaceholder('Select moderators')
.setMinValues(1)
.setMaxValues(5);
// Role select menu
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('assign_roles')
.setPlaceholder('Choose roles to assign')
.setMaxValues(10);
// Channel select menu with type filtering
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('select_channel')
.setPlaceholder('Choose a text channel')
.setChannelTypes(ChannelType.GuildText, ChannelType.GuildNews)
.setMaxValues(1);
// Create message with select menus
const selectRow1 = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(stringSelect);
const selectRow2 = new ActionRowBuilder<UserSelectMenuBuilder>()
.addComponents(userSelect);
await interaction.reply({
content: 'Make your selections:',
components: [selectRow1, selectRow2]
});Pop-up forms for collecting user input with text fields.
/**
* Builder for creating modals
*/
class ModalBuilder {
constructor(data?: Partial<ModalData>);
// Modal configuration
setCustomId(customId: string): this;
setTitle(title: string): this;
addComponents(...components: ActionRowBuilder<ModalActionRowComponentBuilder>[]): this;
setComponents(...components: ActionRowBuilder<ModalActionRowComponentBuilder>[]): this;
spliceComponents(index: number, deleteCount: number, ...components: ActionRowBuilder<ModalActionRowComponentBuilder>[]): this;
toJSON(): APIModalInteractionResponseCallbackData;
static from(other: JSONEncodable<APIModalInteractionResponseCallbackData> | APIModalInteractionResponseCallbackData): ModalBuilder;
}
/**
* Builder for text input components
*/
class TextInputBuilder {
constructor(data?: Partial<TextInputComponentData | APITextInputComponent>);
// Input configuration
setCustomId(customId: string): this;
setLabel(label: string): this;
setStyle(style: TextInputStyle): this;
setPlaceholder(placeholder: string): this;
setValue(value: string): this;
setRequired(required?: boolean): this;
setMinLength(minLength: number): this;
setMaxLength(maxLength: number): this;
toJSON(): APITextInputComponent;
static from(other: JSONEncodable<APITextInputComponent> | APITextInputComponent): TextInputBuilder;
}
/**
* Represents a text input component
*/
class TextInputComponent extends Component<APITextInputComponent> {
readonly type: ComponentType.TextInput;
readonly customId: string;
readonly style: TextInputStyle;
readonly label: string;
readonly minLength: number | null;
readonly maxLength: number | null;
readonly required: boolean;
readonly value: string | null;
readonly placeholder: string | null;
toJSON(): APITextInputComponent;
}
const TextInputStyle: {
Short: 1; // Single-line input
Paragraph: 2; // Multi-line input
};
interface ModalData {
title: string;
custom_id: string;
components: ActionRowData<ModalActionRowComponentData>[];
}
interface TextInputComponentData extends BaseComponentData {
type: ComponentType.TextInput;
custom_id: string;
style: TextInputStyle;
label: string;
min_length?: number;
max_length?: number;
required?: boolean;
value?: string;
placeholder?: string;
}Usage Examples:
// Create a feedback modal
const feedbackModal = new ModalBuilder()
.setCustomId('feedback_modal')
.setTitle('Feedback Form')
.addComponents(
new ActionRowBuilder<TextInputBuilder>()
.addComponents(
new TextInputBuilder()
.setCustomId('feedback_title')
.setLabel('Title')
.setStyle(TextInputStyle.Short)
.setPlaceholder('Brief title for your feedback')
.setRequired(true)
.setMaxLength(100)
),
new ActionRowBuilder<TextInputBuilder>()
.addComponents(
new TextInputBuilder()
.setCustomId('feedback_description')
.setLabel('Description')
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder('Detailed description of your feedback...')
.setRequired(true)
.setMaxLength(2000)
),
new ActionRowBuilder<TextInputBuilder>()
.addComponents(
new TextInputBuilder()
.setCustomId('feedback_rating')
.setLabel('Rating (1-10)')
.setStyle(TextInputStyle.Short)
.setPlaceholder('Rate from 1 to 10')
.setRequired(false)
.setMinLength(1)
.setMaxLength(2)
)
);
// Show modal in response to button interaction
if (interaction.isButton() && interaction.customId === 'show_feedback') {
await interaction.showModal(feedbackModal);
}
// Handle modal submission
if (interaction.isModalSubmit() && interaction.customId === 'feedback_modal') {
const title = interaction.fields.getTextInputValue('feedback_title');
const description = interaction.fields.getTextInputValue('feedback_description');
const rating = interaction.fields.getTextInputValue('feedback_rating') || 'Not provided';
await interaction.reply({
content: `Thank you for your feedback!\nTitle: ${title}\nRating: ${rating}`,
ephemeral: true
});
}Additional component features and utilities for complex interfaces.
/**
* Base component class
* @template DataType The API component data type
*/
class Component<DataType extends APIComponentInMessageActionRow | APIComponentInModalActionRow = APIComponentInMessageActionRow | APIComponentInModalActionRow> {
constructor(data: DataType);
readonly type: ComponentType;
toJSON(): DataType;
valueOf(): string;
}
/**
* Container component for organizing UI elements
*/
class ContainerComponent extends Component<APIContainerComponent> {
readonly type: ComponentType.Container;
readonly components: APIComponentInContainer[];
toJSON(): APIContainerComponent;
}
/**
* Section component for grouping related elements
*/
class SectionComponent extends Component<APISectionComponent> {
readonly type: ComponentType.Section;
readonly components: APIComponentInContainer[];
toJSON(): APISectionComponent;
}
/**
* Separator component for visual separation
*/
class SeparatorComponent extends Component<APISeparatorComponent> {
readonly type: ComponentType.Separator;
readonly spacing: SeparatorSpacingSize;
toJSON(): APISeparatorComponent;
}
/**
* Text display component for formatted text
*/
class TextDisplayComponent extends Component<APITextDisplayComponent> {
readonly type: ComponentType.TextDisplay;
readonly content: string;
toJSON(): APITextDisplayComponent;
}
const ComponentType: {
ActionRow: 1;
Button: 2;
StringSelect: 3;
TextInput: 4;
UserSelect: 5;
RoleSelect: 6;
MentionableSelect: 7;
ChannelSelect: 8;
Container: 9;
Section: 10;
Separator: 11;
TextDisplay: 12;
// ... more component types
};interface BaseComponentData {
type: ComponentType;
}
type AnyComponentBuilder =
| ActionRowBuilder<any>
| ButtonBuilder
| StringSelectMenuBuilder
| UserSelectMenuBuilder
| RoleSelectMenuBuilder
| MentionableSelectMenuBuilder
| ChannelSelectMenuBuilder
| TextInputBuilder;
type MessageActionRowComponentData =
| ButtonComponentData
| StringSelectMenuComponentData
| UserSelectMenuComponentData
| RoleSelectMenuComponentData
| MentionableSelectMenuComponentData
| ChannelSelectMenuComponentData;
type ModalActionRowComponentData = TextInputComponentData;
type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData;
// Component resolvers for finding components by custom ID
interface ComponentResolver {
resolveComponent(customId: string): MessageActionRowComponent | null;
resolveComponents(customId?: string): MessageActionRowComponent[];
}Advanced Usage Examples:
// Complex multi-row interface
const createComplexInterface = () => {
const buttonRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId('action_1')
.setLabel('Action 1')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId('action_2')
.setLabel('Action 2')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId('cancel')
.setLabel('Cancel')
.setStyle(ButtonStyle.Danger)
);
const selectRow = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('category_select')
.setPlaceholder('Choose a category')
.addOptions(
{ label: 'General', value: 'general', description: 'General category' },
{ label: 'Music', value: 'music', description: 'Music related' },
{ label: 'Gaming', value: 'gaming', description: 'Gaming content' }
)
);
const userSelectRow = new ActionRowBuilder<UserSelectMenuBuilder>()
.addComponents(
new UserSelectMenuBuilder()
.setCustomId('user_select')
.setPlaceholder('Select users')
.setMaxValues(5)
);
return [buttonRow, selectRow, userSelectRow];
};
// Dynamic component updating
const updateComponentState = async (interaction: ButtonInteraction) => {
const components = interaction.message.components;
// Find and update button states
const updatedComponents = components.map(row => {
const newRow = new ActionRowBuilder<ButtonBuilder>();
row.components.forEach(component => {
if (component.type === ComponentType.Button) {
const button = ButtonBuilder.from(component);
// Disable the clicked button
if (component.customId === interaction.customId) {
button.setDisabled(true);
}
newRow.addComponents(button);
}
});
return newRow;
});
await interaction.update({
content: 'Button clicked!',
components: updatedComponents
});
};
// Component validation
const validateComponents = (components: ActionRowBuilder[]) => {
components.forEach((row, rowIndex) => {
if (row.components.length > 5) {
throw new Error(`Row ${rowIndex} has too many components (max 5)`);
}
const buttonCount = row.components.filter(c => c instanceof ButtonBuilder).length;
const selectCount = row.components.filter(c =>
c instanceof StringSelectMenuBuilder ||
c instanceof UserSelectMenuBuilder ||
c instanceof RoleSelectMenuBuilder ||
c instanceof ChannelSelectMenuBuilder ||
c instanceof MentionableSelectMenuBuilder
).length;
if (buttonCount > 0 && selectCount > 0) {
throw new Error(`Row ${rowIndex} cannot mix buttons and select menus`);
}
if (selectCount > 1) {
throw new Error(`Row ${rowIndex} cannot have multiple select menus`);
}
});
};Essential builders for attachments and select menu options.
/**
* Builder for message attachments
*/
class AttachmentBuilder {
constructor(attachment: BufferResolvable | Stream, options?: AttachmentData);
// Set attachment metadata
setName(name: string): this;
setDescription(description: string): this;
setSpoiler(spoiler?: boolean): this;
// Static methods
static from(other: JSONEncodable<AttachmentPayload> | BufferResolvable | Stream): AttachmentBuilder;
// Properties
readonly attachment: BufferResolvable | Stream;
readonly name: string | undefined;
readonly description: string | undefined;
readonly spoiler: boolean;
}
/**
* Base builder for select menu options
*/
class SelectMenuOptionBuilder {
constructor(data?: Partial<APISelectMenuOption>);
// Option configuration
setLabel(label: string): this;
setValue(value: string): this;
setDescription(description?: string): this;
setEmoji(emoji: ComponentEmojiResolvable): this;
setDefault(isDefault?: boolean): this;
// Static methods
static from(other: JSONEncodable<APISelectMenuOption> | APISelectMenuOption): SelectMenuOptionBuilder;
// Convert to API format
toJSON(): APISelectMenuOption;
}
interface AttachmentData {
name?: string;
description?: string;
}
interface AttachmentPayload {
id: number;
filename: string;
description?: string;
content_type?: string;
size: number;
url: string;
proxy_url: string;
height?: number | null;
width?: number | null;
ephemeral?: boolean;
}Usage Examples:
// Create attachment builders
const imageAttachment = new AttachmentBuilder('./path/to/image.png', {
name: 'discord-logo.png',
description: 'The Discord logo'
});
const spoilerAttachment = new AttachmentBuilder(buffer)
.setName('secret.txt')
.setSpoiler(true)
.setDescription('Confidential information');
// Send message with attachments
await channel.send({
content: 'Here are some files!',
files: [imageAttachment, spoilerAttachment]
});
// Create generic select menu options
const genericOption = new SelectMenuOptionBuilder()
.setLabel('Generic Option')
.setValue('generic_value')
.setDescription('A generic select menu option')
.setEmoji('π§');
// Use in string select menu
const stringSelect = new StringSelectMenuBuilder()
.setCustomId('generic_select')
.setPlaceholder('Choose an option')
.addOptions(
SelectMenuOptionBuilder.from({
label: 'Option 1',
value: 'opt1',
description: 'First option'
}),
genericOption
);