A Feature-rich Discord Bot Framework for Python with comprehensive API coverage and modern interfaces
—
Interactive components like buttons, select menus, and modals for rich Discord bot interfaces.
Action rows are containers that hold interactive components. Each message can have up to 5 action rows.
from interactions import ActionRow, Button, ButtonStyle
# Create action row with button
button = Button(
style=ButtonStyle.PRIMARY,
label="Click Me!",
custom_id="my_button"
)
action_row = ActionRow(button)
# Send message with components
await ctx.send("Click the button below!", components=[action_row])from interactions import ACTION_ROW_MAX_ITEMS
# Maximum components per action row
max_items = ACTION_ROW_MAX_ITEMS # 5
# Multiple components in one row
buttons = [
Button(style=ButtonStyle.PRIMARY, label=f"Button {i}", custom_id=f"btn_{i}")
for i in range(5)
]
action_row = ActionRow(*buttons)from interactions import Button, ButtonStyle, ActionRow
@slash_command(name="buttons", description="Show different button types")
async def buttons_command(ctx: SlashContext):
# Different button styles
primary_btn = Button(
style=ButtonStyle.PRIMARY,
label="Primary",
custom_id="primary_button"
)
secondary_btn = Button(
style=ButtonStyle.SECONDARY,
label="Secondary",
custom_id="secondary_button"
)
success_btn = Button(
style=ButtonStyle.SUCCESS,
label="Success",
custom_id="success_button"
)
danger_btn = Button(
style=ButtonStyle.DANGER,
label="Danger",
custom_id="danger_button"
)
# Link button (doesn't send interaction)
link_btn = Button(
style=ButtonStyle.LINK,
label="Visit GitHub",
url="https://github.com"
)
row1 = ActionRow(primary_btn, secondary_btn, success_btn)
row2 = ActionRow(danger_btn, link_btn)
await ctx.send("Choose a button:", components=[row1, row2])from interactions import Button, ButtonStyle, PartialEmoji
@slash_command(name="emoji_buttons", description="Buttons with emojis")
async def emoji_buttons(ctx: SlashContext):
# Unicode emoji
thumbs_up = Button(
style=ButtonStyle.SUCCESS,
label="Like",
emoji="👍",
custom_id="like_btn"
)
# Custom emoji (if bot has access)
custom_emoji = PartialEmoji(name="custom_emoji", id=12345678901234567)
custom_btn = Button(
style=ButtonStyle.PRIMARY,
emoji=custom_emoji,
custom_id="custom_btn"
)
# Emoji only (no label)
reaction_btn = Button(
style=ButtonStyle.SECONDARY,
emoji="❤️",
custom_id="heart_btn"
)
row = ActionRow(thumbs_up, custom_btn, reaction_btn)
await ctx.send("React with buttons!", components=[row])@slash_command(name="disabled", description="Show disabled buttons")
async def disabled_buttons(ctx: SlashContext):
enabled_btn = Button(
style=ButtonStyle.PRIMARY,
label="Enabled",
custom_id="enabled_btn"
)
disabled_btn = Button(
style=ButtonStyle.SECONDARY,
label="Disabled",
custom_id="disabled_btn",
disabled=True
)
row = ActionRow(enabled_btn, disabled_btn)
await ctx.send("One button is disabled:", components=[row])from interactions import component_callback, ComponentContext
@component_callback("my_button")
async def button_callback(ctx: ComponentContext):
"""Handle button click"""
await ctx.send("Button clicked!", ephemeral=True)
@component_callback("like_btn")
async def like_button(ctx: ComponentContext):
"""Handle like button"""
user = ctx.author
await ctx.send(f"{user.mention} liked this!", ephemeral=True)
# Multiple button handler
@component_callback("btn_1", "btn_2", "btn_3")
async def multi_button_handler(ctx: ComponentContext):
"""Handle multiple buttons with one callback"""
button_id = ctx.custom_id
await ctx.send(f"You clicked {button_id}!", ephemeral=True)from interactions import StringSelectMenu, StringSelectOption, ActionRow
@slash_command(name="select", description="Choose from options")
async def select_command(ctx: SlashContext):
options = [
StringSelectOption(
label="Option 1",
value="opt_1",
description="First option",
emoji="1️⃣"
),
StringSelectOption(
label="Option 2",
value="opt_2",
description="Second option",
emoji="2️⃣"
),
StringSelectOption(
label="Option 3",
value="opt_3",
description="Third option",
emoji="3️⃣",
default=True # Pre-selected
)
]
select_menu = StringSelectMenu(
*options,
placeholder="Choose an option...",
min_values=1,
max_values=2, # Allow selecting up to 2 options
custom_id="string_select"
)
row = ActionRow(select_menu)
await ctx.send("Make your selection:", components=[row])
@component_callback("string_select")
async def select_callback(ctx: ComponentContext):
"""Handle select menu interaction"""
selected_values = ctx.values # List of selected values
await ctx.send(f"You selected: {', '.join(selected_values)}", ephemeral=True)from interactions import UserSelectMenu
@slash_command(name="pick_user", description="Pick users")
async def pick_user_command(ctx: SlashContext):
user_select = UserSelectMenu(
placeholder="Select users...",
min_values=1,
max_values=5, # Allow selecting up to 5 users
custom_id="user_select"
)
row = ActionRow(user_select)
await ctx.send("Pick some users:", components=[row])
@component_callback("user_select")
async def user_select_callback(ctx: ComponentContext):
"""Handle user selection"""
# ctx.resolved contains the resolved users
selected_users = ctx.resolved.users if ctx.resolved else []
user_mentions = [user.mention for user in selected_users.values()]
await ctx.send(f"Selected users: {', '.join(user_mentions)}", ephemeral=True)from interactions import RoleSelectMenu
@slash_command(name="pick_role", description="Pick roles")
async def pick_role_command(ctx: SlashContext):
role_select = RoleSelectMenu(
placeholder="Select roles...",
min_values=1,
max_values=3,
custom_id="role_select"
)
row = ActionRow(role_select)
await ctx.send("Pick some roles:", components=[row])
@component_callback("role_select")
async def role_select_callback(ctx: ComponentContext):
"""Handle role selection"""
selected_roles = ctx.resolved.roles if ctx.resolved else []
role_mentions = [role.mention for role in selected_roles.values()]
await ctx.send(f"Selected roles: {', '.join(role_mentions)}", ephemeral=True)from interactions import ChannelSelectMenu, ChannelType
@slash_command(name="pick_channel", description="Pick channels")
async def pick_channel_command(ctx: SlashContext):
channel_select = ChannelSelectMenu(
placeholder="Select channels...",
min_values=1,
max_values=2,
channel_types=[ChannelType.GUILD_TEXT, ChannelType.GUILD_VOICE], # Limit channel types
custom_id="channel_select"
)
row = ActionRow(channel_select)
await ctx.send("Pick some channels:", components=[row])
@component_callback("channel_select")
async def channel_select_callback(ctx: ComponentContext):
"""Handle channel selection"""
selected_channels = ctx.resolved.channels if ctx.resolved else []
channel_mentions = [f"#{channel.name}" for channel in selected_channels.values()]
await ctx.send(f"Selected channels: {', '.join(channel_mentions)}", ephemeral=True)from interactions import MentionableSelectMenu
@slash_command(name="pick_mentionable", description="Pick users or roles")
async def pick_mentionable_command(ctx: SlashContext):
mentionable_select = MentionableSelectMenu(
placeholder="Select users or roles...",
min_values=1,
max_values=5,
custom_id="mentionable_select"
)
row = ActionRow(mentionable_select)
await ctx.send("Pick users or roles:", components=[row])
@component_callback("mentionable_select")
async def mentionable_select_callback(ctx: ComponentContext):
"""Handle mentionable selection"""
mentions = []
if ctx.resolved:
# Add selected users
if ctx.resolved.users:
mentions.extend([user.mention for user in ctx.resolved.users.values()])
# Add selected roles
if ctx.resolved.roles:
mentions.extend([role.mention for role in ctx.resolved.roles.values()])
await ctx.send(f"Selected: {', '.join(mentions)}", ephemeral=True)from interactions import Modal, InputText, TextStyles
@slash_command(name="feedback", description="Submit feedback")
async def feedback_command(ctx: SlashContext):
modal = Modal(
InputText(
label="Feedback Title",
placeholder="Enter a title for your feedback...",
custom_id="title",
style=TextStyles.SHORT,
required=True,
max_length=100
),
InputText(
label="Feedback Details",
placeholder="Describe your feedback in detail...",
custom_id="details",
style=TextStyles.PARAGRAPH,
required=True,
max_length=1000
),
InputText(
label="Contact Email",
placeholder="your.email@example.com",
custom_id="email",
style=TextStyles.SHORT,
required=False
),
title="Feedback Form",
custom_id="feedback_modal"
)
await ctx.send_modal(modal)
@modal_callback("feedback_modal")
async def feedback_modal_callback(ctx: ModalContext):
"""Handle feedback modal submission"""
title = ctx.responses["title"]
details = ctx.responses["details"]
email = ctx.responses["email"]
embed = Embed(
title="Feedback Received",
description=f"**Title:** {title}\n\n**Details:** {details}",
color=0x00ff00
)
if email:
embed.add_field(name="Contact", value=email, inline=False)
embed.set_footer(text=f"Submitted by {ctx.author}")
await ctx.send("Thank you for your feedback!", embed=embed, ephemeral=True)@slash_command(name="edit_profile", description="Edit your profile")
async def edit_profile_command(ctx: SlashContext):
# Get user's current info (from database, etc.)
current_bio = get_user_bio(ctx.author.id)
current_status = get_user_status(ctx.author.id)
modal = Modal(
InputText(
label="Bio",
placeholder="Tell us about yourself...",
custom_id="bio",
style=TextStyles.PARAGRAPH,
value=current_bio, # Pre-fill with current value
max_length=500
),
InputText(
label="Status",
placeholder="What's your current status?",
custom_id="status",
style=TextStyles.SHORT,
value=current_status,
max_length=100
),
title="Edit Profile",
custom_id="profile_modal"
)
await ctx.send_modal(modal)from interactions import ShortText, ParagraphText
# Alternative syntax using specific input classes
modal = Modal(
ShortText(
label="Username",
placeholder="Enter username...",
custom_id="username",
min_length=3,
max_length=20
),
ParagraphText(
label="Description",
placeholder="Describe yourself...",
custom_id="description",
min_length=10,
max_length=500
),
title="User Registration",
custom_id="register_modal"
)@component_callback("counter_btn")
async def counter_callback(ctx: ComponentContext):
"""Update button based on state"""
# Get current count (from message, database, etc.)
current_count = get_counter_value(ctx.message.id)
new_count = current_count + 1
# Update the button
updated_button = Button(
style=ButtonStyle.PRIMARY,
label=f"Count: {new_count}",
custom_id="counter_btn"
)
row = ActionRow(updated_button)
# Edit the message with updated components
await ctx.edit_origin(
content=f"Button clicked {new_count} times!",
components=[row]
)
# Update stored value
set_counter_value(ctx.message.id, new_count)import json
@slash_command(name="poll", description="Create a poll")
@slash_option(name="question", description="Poll question", required=True, opt_type=OptionType.STRING)
async def create_poll(ctx: SlashContext, question: str):
"""Create a poll with voting buttons"""
# Create buttons with embedded state
yes_btn = Button(
style=ButtonStyle.SUCCESS,
label="Yes (0)",
custom_id="poll_yes",
emoji="✅"
)
no_btn = Button(
style=ButtonStyle.DANGER,
label="No (0)",
custom_id="poll_no",
emoji="❌"
)
row = ActionRow(yes_btn, no_btn)
embed = Embed(
title="📊 Poll",
description=question,
color=0x0099ff
)
message = await ctx.send(embed=embed, components=[row])
# Initialize poll data
init_poll_data(message.id, {"yes": 0, "no": 0, "voters": []})
@component_callback("poll_yes", "poll_no")
async def poll_vote_callback(ctx: ComponentContext):
"""Handle poll voting"""
vote_type = ctx.custom_id.split("_")[1] # "yes" or "no"
user_id = ctx.author.id
# Get current poll data
poll_data = get_poll_data(ctx.message.id)
# Check if user already voted
if user_id in poll_data["voters"]:
await ctx.send("You already voted in this poll!", ephemeral=True)
return
# Record vote
poll_data[vote_type] += 1
poll_data["voters"].append(user_id)
# Update buttons with new counts
yes_btn = Button(
style=ButtonStyle.SUCCESS,
label=f"Yes ({poll_data['yes']})",
custom_id="poll_yes",
emoji="✅"
)
no_btn = Button(
style=ButtonStyle.DANGER,
label=f"No ({poll_data['no']})",
custom_id="poll_no",
emoji="❌"
)
row = ActionRow(yes_btn, no_btn)
# Update message
await ctx.edit_origin(components=[row])
# Save poll data
save_poll_data(ctx.message.id, poll_data)
await ctx.send(f"Voted {vote_type.title()}!", ephemeral=True)@component_callback("temp_button")
async def temp_button_callback(ctx: ComponentContext):
"""Button that removes itself after use"""
await ctx.send("Button used! Removing it now...", ephemeral=True)
# Remove components by editing message with empty components
await ctx.edit_origin(
content="Button was used and removed.",
components=[]
)@slash_command(name="admin_panel", description="Show admin panel")
async def admin_panel(ctx: SlashContext):
"""Show different components based on user permissions"""
components = []
# Basic buttons for everyone
info_btn = Button(
style=ButtonStyle.SECONDARY,
label="Server Info",
custom_id="server_info"
)
components.append(info_btn)
# Admin-only buttons
if ctx.author.guild_permissions.ADMINISTRATOR:
admin_btn = Button(
style=ButtonStyle.DANGER,
label="Admin Actions",
custom_id="admin_actions"
)
components.append(admin_btn)
# Moderator buttons
if ctx.author.guild_permissions.MANAGE_MESSAGES:
mod_btn = Button(
style=ButtonStyle.PRIMARY,
label="Mod Tools",
custom_id="mod_tools"
)
components.append(mod_btn)
# Organize into rows (max 5 per row)
rows = []
for i in range(0, len(components), 5):
row_components = components[i:i+5]
rows.append(ActionRow(*row_components))
await ctx.send("Control Panel:", components=rows)from interactions import spread_to_rows
# Automatically organize components into action rows
buttons = [
Button(style=ButtonStyle.PRIMARY, label=f"Button {i}", custom_id=f"btn_{i}")
for i in range(8) # 8 buttons
]
# Spread across multiple rows (5 per row max)
rows = spread_to_rows(buttons, max_in_row=5)
# Results in 2 rows: first with 5 buttons, second with 3 buttons
await ctx.send("Multiple rows of buttons:", components=rows)from interactions import get_components_ids
# Extract all custom IDs from components
components = [
ActionRow(
Button(style=ButtonStyle.PRIMARY, label="A", custom_id="btn_a"),
Button(style=ButtonStyle.SECONDARY, label="B", custom_id="btn_b")
),
ActionRow(
StringSelectMenu(
StringSelectOption(label="Option", value="val"),
custom_id="select_menu"
)
)
]
component_ids = get_components_ids(components)
# Returns: ["btn_a", "btn_b", "select_menu"]from interactions import (
ACTION_ROW_MAX_ITEMS,
SELECTS_MAX_OPTIONS,
SELECT_MAX_NAME_LENGTH
)
# Component limits
ACTION_ROW_MAX_ITEMS # 5 - Max components per action row
SELECTS_MAX_OPTIONS # 25 - Max options in select menu
SELECT_MAX_NAME_LENGTH # 100 - Max length for select option names@listen(events.ComponentError)
async def on_component_error(event: events.ComponentError):
"""Handle component interaction errors"""
ctx = event.ctx
error = event.error
print(f"Component error in {ctx.custom_id}: {error}")
# Try to send error message to user
try:
await ctx.send("An error occurred with this component.", ephemeral=True)
except:
pass # Interaction may have already been responded to
@listen(events.ModalError)
async def on_modal_error(event: events.ModalError):
"""Handle modal submission errors"""
ctx = event.ctx
error = event.error
print(f"Modal error in {ctx.custom_id}: {error}")
try:
await ctx.send("An error occurred processing your form.", ephemeral=True)
except:
pass@component_callback("risky_button")
async def risky_button_callback(ctx: ComponentContext):
"""Button callback with error handling"""
try:
# Risky operation that might fail
result = await perform_risky_operation(ctx.author.id)
await ctx.send(f"Operation successful: {result}", ephemeral=True)
except ValueError as e:
await ctx.send(f"Invalid input: {e}", ephemeral=True)
except PermissionError:
await ctx.send("You don't have permission for this action.", ephemeral=True)
except Exception as e:
await ctx.send("An unexpected error occurred.", ephemeral=True)
# Log the error
print(f"Unexpected error in risky_button: {e}")Install with Tessl CLI
npx tessl i tessl/pypi-discord-py-interactions