CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-discord-py-interactions

A Feature-rich Discord Bot Framework for Python with comprehensive API coverage and modern interfaces

Pending
Overview
Eval results
Files

components.mddocs/

User Interface Components

Interactive components like buttons, select menus, and modals for rich Discord bot interfaces.

Action Rows

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])

Action Row Limits

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)

Buttons

Basic 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])

Buttons with Emojis

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])

Disabled Buttons

@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])

Button Callbacks

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)

Select Menus

String Select Menu

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)

User Select Menu

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)

Role Select Menu

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)

Channel Select Menu

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)

Mentionable Select Menu

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)

Modals

Basic Modal

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)

Pre-filled Modal

@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)

Modal Input Types

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

Advanced Component Patterns

Dynamic Component Updates

@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)

Component State Management

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 Cleanup

@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=[]
    )

Conditional Component Display

@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)

Component Utility Functions

Spread Components to 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)

Get Component IDs

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"]

Component Limits & Constants

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

Error Handling

Component Error Events

@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

Try-Catch in Callbacks

@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

docs

client.md

commands.md

components.md

discord-models.md

events.md

extensions.md

index.md

tile.json