Buttons & Components
Components are interactive elements you can add to messages: buttons, select menus, and more. They enable rich user interfaces without message parsing.
Component Types
Discord supports several component types:
- Buttons - Clickable buttons with styles
- String Select - Dropdown menu (text options)
- User Select - Dropdown for selecting users
- Role Select - Dropdown for selecting roles
- Channel Select - Dropdown for selecting channels
- Mentionable Select - Dropdown for users and roles
Buttons
Button Styles
:primary # Blurple (style: 1) - Custom ID
:secondary # Gray (style: 2) - Custom ID
:success # Green (style: 3) - Custom ID
:danger # Red (style: 4) - Custom ID
:link # URL buttons (style: 5) - Opens URL
Creating Buttons
interaction.respond(content: 'Choose an action:') do |builder|
builder.components do |row|
# Primary button
row.button(
style: :primary,
label: 'Approve',
custom_id: 'approve_btn',
emoji: '✅'
)
# Danger button
row.button(
style: :danger,
label: 'Reject',
custom_id: 'reject_btn',
emoji: '❌'
)
# Link button (opens URL)
row.button(
style: :link,
label: 'View Docs',
url: 'https://example.com'
)
end
end
Handling Button Clicks
bot.on(:button_click) do |interaction|
case interaction.custom_id
when 'approve_btn'
interaction.update_message(
content: '✅ Approved!',
components: [] # Remove buttons
)
when 'reject_btn'
interaction.update_message(
content: '❌ Rejected',
components: []
)
end
end
Disabled Buttons
row.button(
style: :primary,
label: 'Processing...',
custom_id: 'processing',
disabled: true # Grayed out, not clickable
)
Button with Emoji
row.button(
style: :success,
label: 'Like',
custom_id: 'like_btn',
emoji: { name: '👍' }
)
# Custom emoji
row.button(
style: :primary,
label: 'React',
custom_id: 'react_btn',
emoji: { name: 'party', id: '123456789', animated: false }
)
Select Menus
String Select (Dropdown)
interaction.respond(content: 'Pick a color:') do |builder|
builder.components do |row|
row.string_select(
custom_id: 'color_select',
placeholder: 'Choose a color',
min_values: 1, # Minimum selections
max_values: 1, # Maximum selections
options: [
{
label: 'Red',
value: 'red',
description: 'The color of passion',
emoji: { name: '🔴' }
},
{
label: 'Green',
value: 'green',
description: 'The color of nature',
emoji: { name: '🟢' }
},
{
label: 'Blue',
value: 'blue',
description: 'The color of sky',
emoji: { name: '🔵' }
}
]
)
end
end
# Handle selection
bot.on(:string_select) do |interaction|
next unless interaction.custom_id == 'color_select'
selected = interaction.selected_values.first
interaction.update_message(content: "You selected: #{selected}")
end
Multi-Select
row.string_select(
custom_id: 'roles_select',
placeholder: 'Select roles to assign',
min_values: 0,
max_values: 5, # Can select up to 5
options: available_roles.map { |r| { label: r.name, value: r.id } }
)
User Select
interaction.respond(content: 'Who should be the leader?') do |builder|
builder.components do |row|
row.user_select(
custom_id: 'leader_select',
placeholder: 'Select a user',
max_values: 1
)
end
end
bot.on(:user_select) do |interaction|
next unless interaction.custom_id == 'leader_select'
selected_user_id = interaction.selected_values.first
selected_user = interaction.resolved.users[selected_user_id]
interaction.update_message(
content: "Leader selected: #{selected_user.mention}"
)
end
Role Select
row.role_select(
custom_id: 'role_select',
placeholder: 'Choose a role'
)
bot.on(:role_select) do |interaction|
role_id = interaction.selected_values.first
role = interaction.resolved.roles[role_id]
interaction.respond(
content: "Selected role: #{role.mention}",
ephemeral: true
)
end
Channel Select
row.channel_select(
custom_id: 'channel_select',
placeholder: 'Select destination channel',
channel_types: [0, 5], # Only text and news channels
max_values: 1
)
bot.on(:channel_select) do |interaction|
channel_id = interaction.selected_values.first
channel = interaction.resolved.channels[channel_id]
interaction.respond(
content: "Selected: #{channel.mention}",
ephemeral: true
)
end
Mentionable Select
row.mentionable_select(
custom_id: 'notify_select',
placeholder: 'Who to notify?',
max_values: 10 # Users and/or roles
)
bot.on(:mentionable_select) do |interaction|
# Can contain user IDs and role IDs
selected = interaction.selected_values
mentions = selected.map { |id| "<@#{id}>" }.join(' ')
interaction.respond(content: "Will notify: #{mentions}")
end
Action Rows
Components must be placed in "Action Rows" (up to 5 per message, 5 components per row):
interaction.respond(content: 'Multiple rows:') do |builder|
builder.components do |row1|
# Row 1: 3 buttons
row1.button(style: :primary, label: '1', custom_id: 'btn1')
row1.button(style: :primary, label: '2', custom_id: 'btn2')
row1.button(style: :primary, label: '3', custom_id: 'btn3')
end
builder.components do |row2|
# Row 2: 1 select menu (selects take full row)
row2.string_select(
custom_id: 'select1',
placeholder: 'Choose',
options: [{ label: 'A', value: 'a' }]
)
end
end
Advanced Patterns
Paginated Results
class Paginator
def initialize(bot, items, per_page: 10)
@bot = bot
@items = items
@per_page = per_page
end
def send(interaction, page: 0)
total_pages = (@items.length.to_f / @per_page).ceil
start_idx = page * @per_page
end_idx = start_idx + @per_page
page_items = @items[start_idx...end_idx]
content = page_items.map { |i| "• #{i}".join("\n")
interaction.respond(content: content) do |builder|
builder.components do |row|
row.button(
style: :secondary,
label: '◀ Previous',
custom_id: "page_#{page - 1}",
disabled: page == 0
)
row.button(
style: :secondary,
label: "#{page + 1} / #{total_pages}",
custom_id: 'page_indicator',
disabled: true
)
row.button(
style: :secondary,
label: 'Next ▶',
custom_id: "page_#{page + 1}",
disabled: page >= total_pages - 1
)
end
end
end
end
# Handle pagination
bot.on(:button_click) do |interaction|
if interaction.custom_id.start_with?('page_')
page = interaction.custom_id.split('_')[1].to_i
# Re-render with new page
# (Need to track what was being paginated)
end
end
Confirmation Dialog
def confirm(interaction, message, on_confirm:, on_cancel: nil)
interaction.respond(content: message) do |builder|
builder.components do |row|
row.button(
style: :success,
label: '✅ Confirm',
custom_id: 'confirm'
)
row.button(
style: :danger,
label: '❌ Cancel',
custom_id: 'cancel'
)
end
end
# Wait for response
response = bot.wait_for(:button_click, timeout: 60) do |i|
i.message.id == interaction.message.id &&
i.user.id == interaction.user.id
end
if response && response.custom_id == 'confirm'
on_confirm.call(response)
else
on_cancel&.call(response)
end
end
Dynamic Select Options
bot.slash('roles', 'Assign roles') do |cmd|
cmd.handler do |interaction|
# Get available roles
roles = bot.guild_roles(interaction.guild_id)
.reject { |r| r.managed? }
.reject { |r| r.name == '@everyone' }
.first(25) # Discord limit
options = roles.map do |role|
{
label: role.name,
value: role.id,
description: "#{role.members.length} members",
emoji: { name: '🏷️' }
}
end
interaction.respond(content: 'Select roles:') do |builder|
builder.components do |row|
row.string_select(
custom_id: 'assign_roles',
placeholder: 'Choose roles',
min_values: 0,
max_values: [roles.length, 5].min,
options: options
)
end
end
end
end
Component V2
Discord's new Component V2 system (experimental):
# Container with multiple components
interaction.respond(content: '') do |builder|
builder.container(accent_color: 0x7289da) do |container|
container.text_display(content: '# Welcome')
container.text_display(content: 'Choose an option:')
container.action_row do |row|
row.button(style: :primary, label: 'Option 1', custom_id: 'opt1')
row.button(style: :primary, label: 'Option 2', custom_id: 'opt2')
end
container.separator
container.text_display(content: '_Powered by DiscordRDA_')
end
end
Best Practices
- Use clear labels - Users should understand what clicking does
- Group related buttons - Logical organization in rows
- Disable after use - Prevent double-clicks on important actions
- Handle timeouts - Remove or disable components after time
- Use appropriate styles - Green for success, red for danger
- Limit options - Max 25 options in selects, 5 buttons per row
Complete Example
require 'discord_rda'
bot = DiscordRDA::Bot.new(
token: ENV['DISCORD_TOKEN'],
application_id: ENV['DISCORD_APP_ID'],
intents: [:guilds]
)
# Poll command with buttons
bot.slash('poll', 'Create a poll') do |cmd|
cmd.string('question', 'Poll question', required: true)
cmd.string('option1', 'First option', required: true)
cmd.string('option2', 'Second option', required: true)
cmd.handler do |interaction|
question = interaction.option('question')
opt1 = interaction.option('option1')
opt2 = interaction.option('option2')
interaction.respond do |builder|
builder.content = "📊 **#{question}**"
builder.components do |row|
row.button(
style: :primary,
label: opt1,
custom_id: 'vote_1',
emoji: { name: '1️⃣' }
)
row.button(
style: :primary,
label: opt2,
custom_id: 'vote_2',
emoji: { name: '2️⃣' }
)
end
end
end
end
# Handle votes
votes = Hash.new(0)
bot.on(:button_click) do |interaction|
case interaction.custom_id
when 'vote_1'
votes[interaction.user.id] = 1
interaction.respond(
content: "Voted for option 1!",
ephemeral: true
)
when 'vote_2'
votes[interaction.user.id] = 2
interaction.respond(
content: "Voted for option 2!",
ephemeral: true
)
end
end
# Help command with select menu
bot.slash('help', 'Get help') do |cmd|
cmd.handler do |interaction|
interaction.respond(content: 'Select a topic:') do |builder|
builder.components do |row|
row.string_select(
custom_id: 'help_topic',
placeholder: 'Choose topic',
options: [
{ label: 'Getting Started', value: 'start', emoji: { name: '🚀' } },
{ label: 'Commands', value: 'commands', emoji: { name: '⌨️' } },
{ label: 'FAQ', value: 'faq', emoji: { name: '❓' } },
{ label: 'Support', value: 'support', emoji: { name: '🆘' } }
]
)
end
end
end
end
bot.on(:string_select) do |interaction|
next unless interaction.custom_id == 'help_topic'
topic = interaction.selected_values.first
content = case topic
when 'start'
'Welcome! Get started by reading the docs.'
when 'commands'
'Available commands: /help, /poll, /info'
when 'faq'
'Q: Is this free? A: Yes!'
when 'support'
'Join our support server: discord.gg/example'
end
interaction.update_message(content: content)
end
bot.run
Next Steps
- Create Modals - Form inputs for complex data
- Add Autocomplete - Smart suggestions
- Learn Interactions - Full interaction API