Skip to main content
Discord components let users interact with your bot through buttons, select menus, and other UI elements.

Action Rows

Components must be placed in action rows. Each message can have up to 5 action rows.
const row = actionRow()
  .addComponents(
    button().setCustomId('yes').setLabel('Yes').setStyle(ButtonStyle.Success),
    button().setCustomId('no').setLabel('No').setStyle(ButtonStyle.Danger)
  )

await ctx.reply({
  content: 'Do you agree?',
  components: [row.toJSON()]
})

Buttons

Basic Button

const btn = button()
  .setCustomId('click_me')
  .setLabel('Click Me')
  .setStyle(ButtonStyle.Primary)

Button Styles

button()
  .setCustomId('primary')
  .setLabel('Primary')
  .setStyle(ButtonStyle.Primary)  // Blurple
Buttons that open URLs don’t need custom IDs:
button()
  .setUrl('https://flora.dev')
  .setLabel('Documentation')

Button with Emoji

button()
  .setCustomId('like')
  .setLabel('Like')
  .setEmoji({ name: '👍' })
  .setStyle(ButtonStyle.Primary)

Disabled Button

button()
  .setCustomId('disabled')
  .setLabel('Unavailable')
  .setDisabled(true)
  .setStyle(ButtonStyle.Secondary)

Select Menus

String Select

Let users pick from predefined options:
const select = stringSelect('role_select')
  .setPlaceholder('Choose a role')
  .addOptions(
    { label: 'Developer', value: 'dev', emoji: { name: '💻' } },
    { label: 'Designer', value: 'design', emoji: { name: '🎨' } },
    { label: 'Manager', value: 'manager', emoji: { name: '📊' } }
  )

const row = actionRow().addComponents(select)
await ctx.reply({
  content: 'Select your role:',
  components: [row.toJSON()]
})

User Select

Let users pick other users:
const select = userSelect('user_picker')
  .setPlaceholder('Select a user')
  .setMinValues(1)
  .setMaxValues(5)

Role Select

Let users pick roles:
const select = roleSelect('role_picker')
  .setPlaceholder('Select roles')
  .setMaxValues(3)

Channel Select

Let users pick channels:
const select = channelSelect('channel_picker')
  .setPlaceholder('Select a channel')

Mentionable Select

Let users pick users or roles:
const select = mentionableSelect('mention_picker')
  .setPlaceholder('Select users or roles')

Input Text

Text input fields (used in modals):
const input = inputText('feedback_text')
  .setStyle(InputTextStyles.Paragraph)
  .setPlaceholder('Enter your feedback...')
  .setMinLength(10)
  .setMaxLength(500)
  .setRequired(true)
inputText('name')
  .setStyle(InputTextStyles.Short)  // Single line
  .setPlaceholder('Your name')

Components V2

Discord’s new component system supports advanced layouts. Use the IS_COMPONENTS_V2 flag:
const card = container()
  .setAccentColor(0x3366ff)
  .addComponents(
    section()
      .addComponents(textDisplay('Flora Runtime'))
      .setAccessory(thumbnail('https://example.com/logo.png'))
  )

await ctx.reply({
  components: [card.toJSON()],
  flags: MessageFlags.IS_COMPONENTS_V2
})

Container

Top-level wrapper for V2 components:
container()
  .setAccentColor(0x3366ff)
  .setSpoiler(true)
  .addComponents(
    // sections, separators, media galleries, etc.
  )

Section

Groups content with optional accessory:
section()
  .addComponents(
    textDisplay('Welcome to Flora!'),
    textDisplay('Build powerful Discord bots')
  )
  .setAccessory(thumbnail('https://example.com/icon.png'))

Text Display

textDisplay('This is display text')

Thumbnail

thumbnail('https://example.com/image.png')
  .setDescription('Image description')
  .setSpoiler(true)
Display multiple images:
mediaGallery()
  .addItem('https://example.com/img1.png', { description: 'First image' })
  .addItem('https://example.com/img2.png', { spoiler: true })
  .addItem('https://example.com/img3.png')

Separator

Visual divider:
separator(true)  // With divider line
separator(false) // Just spacing

separator()
  .setDivider(true)
  .setSpacing('large')

File

file('https://example.com/document.pdf')
  .setSpoiler(true)

Label

Label for form elements:
label('Email Address')
  .setDescription('Your contact email')
  .setComponent(inputText('email'))

File Upload

Let users upload files:
fileUpload('attachment_input')
  .setMinValues(1)
  .setMaxValues(5)
  .setRequired(true)

Handling Interactions

Listen for component interactions:
on('componentInteraction', async (ctx) => {
  const customId = ctx.msg.data?.custom_id
  
  if (customId === 'yes') {
    await ctx.reply({ content: 'You clicked Yes!', ephemeral: true })
  } else if (customId === 'no') {
    await ctx.reply({ content: 'You clicked No!', ephemeral: true })
  }
})

Select Menu Values

on('componentInteraction', async (ctx) => {
  if (ctx.msg.data?.custom_id === 'role_select') {
    const values = ctx.msg.data?.values || []
    await ctx.reply({
      content: `You selected: ${values.join(', ')}`,
      ephemeral: true
    })
  }
})

Complete Example

const confirm = slash({
  name: 'confirm',
  description: 'Show a confirmation dialog',
  run: async (ctx) => {
    const row = actionRow().addComponents(
      button()
        .setCustomId('confirm_yes')
        .setLabel('Confirm')
        .setStyle(ButtonStyle.Success),
      button()
        .setCustomId('confirm_no')
        .setLabel('Cancel')
        .setStyle(ButtonStyle.Danger)
    )
    
    await ctx.reply({
      content: 'Are you sure you want to proceed?',
      components: [row.toJSON()]
    })
  }
})

on('componentInteraction', async (ctx) => {
  const customId = ctx.msg.data?.custom_id
  
  if (customId === 'confirm_yes') {
    await ctx.reply({ content: 'Action confirmed!', ephemeral: true })
  } else if (customId === 'confirm_no') {
    await ctx.reply({ content: 'Action cancelled.', ephemeral: true })
  }
})

createBot({ slashCommands: [confirm] })

Available Builders

actionRow()
ActionRowBuilder
Container for up to 5 buttons or 1 select menu.
button()
ButtonBuilder
Interactive button.
stringSelect(id)
StringSelectMenuBuilder
Select menu with custom options.
userSelect(id)
UserSelectMenuBuilder
Select menu for users.
roleSelect(id)
RoleSelectMenuBuilder
Select menu for roles.
channelSelect(id)
ChannelSelectMenuBuilder
Select menu for channels.
mentionableSelect(id)
MentionableSelectMenuBuilder
Select menu for users and roles.
inputText(id)
InputTextBuilder
Text input field.
container()
ContainerBuilder
V2 component container.
section()
SectionBuilder
V2 content section.
textDisplay(text)
TextDisplayBuilder
V2 text display.
thumbnail(url)
ThumbnailBuilder
V2 thumbnail image.
V2 image gallery.
separator()
SeparatorBuilder
V2 visual divider.
file(url)
FileBuilder
V2 file attachment.
label(text)
LabelBuilder
V2 form label.
fileUpload(id)
FileUploadBuilder
V2 file upload input.

Best Practices

1

Use descriptive custom IDs

Make IDs descriptive: delete_message_123 instead of btn1.
2

Limit button count

Max 5 buttons per row, 25 per message. Keep interfaces simple.
3

Provide visual feedback

Always reply to interactions, even if ephemeral.
4

Use appropriate styles

Match button colors to actions: red for delete, green for confirm.

Build docs developers (and LLMs) love