CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-discord-py

A modern, feature-rich, and async-ready API wrapper for Discord written in Python

Pending
Overview
Eval results
Files

user-interface.mddocs/

User Interface Components

Interactive UI elements that enable rich user interactions beyond traditional text commands. Discord.py provides buttons, select menus, modals, and views for creating sophisticated bot interfaces directly integrated with Discord's native UI.

Capabilities

Views & Component Containers

Views manage collections of interactive components and handle their lifecycle.

class View:
    """
    Container for UI components with interaction handling.
    
    Parameters:
    - timeout: Seconds before view times out (None for no timeout)
    """
    def __init__(self, *, timeout: Optional[float] = 180.0): ...
    
    timeout: Optional[float]  # Timeout in seconds
    children: List[Item]  # Child components
    
    def add_item(self, item: Item) -> View:
        """Add a component to the view."""
    
    def remove_item(self, item: Item) -> View:
        """Remove a component from the view."""
    
    def clear_items(self) -> View:
        """Remove all components from the view."""
    
    def get_item(self, custom_id: str) -> Optional[Item]:
        """Get component by custom_id."""
    
    @property
    def is_finished(self) -> bool:
        """Whether the view has finished (timed out or stopped)."""
    
    @property
    def is_persistent(self) -> bool:
        """Whether the view is persistent (no timeout)."""
    
    def stop(self) -> None:
        """Stop the view and disable all components."""
    
    async def wait(self) -> bool:
        """Wait for the view to finish (returns True if not timed out)."""
    
    # Lifecycle hooks
    async def on_timeout(self) -> None:
        """Called when view times out."""
    
    async def on_error(self, interaction: Interaction, error: Exception, item: Item) -> None:
        """Called when component interaction raises an exception."""
    
    # Interaction check
    async def interaction_check(self, interaction: Interaction) -> bool:
        """Check if interaction should be processed."""
        return True

class Modal:
    """
    Modal dialog for collecting user input.
    
    Parameters:
    - title: Modal title displayed to user
    - timeout: Seconds before modal times out
    - custom_id: Custom ID for the modal
    """
    def __init__(
        self, 
        *, 
        title: str, 
        timeout: Optional[float] = None, 
        custom_id: str = None
    ): ...
    
    title: str  # Modal title
    timeout: Optional[float]  # Timeout in seconds
    custom_id: str  # Custom ID
    children: List[TextInput]  # Text input components
    
    def add_item(self, item: TextInput) -> Modal:
        """Add a text input to the modal."""
    
    def remove_item(self, item: TextInput) -> Modal:
        """Remove a text input from the modal."""
    
    def clear_items(self) -> Modal:
        """Remove all text inputs from the modal."""
    
    def stop(self) -> None:
        """Stop the modal."""
    
    async def wait(self) -> bool:
        """Wait for the modal to be submitted."""
    
    # Lifecycle hooks
    async def on_submit(self, interaction: Interaction) -> None:
        """Called when modal is submitted."""
    
    async def on_timeout(self) -> None:
        """Called when modal times out."""
    
    async def on_error(self, interaction: Interaction, error: Exception) -> None:
        """Called when modal interaction raises an exception."""
    
    # Interaction check
    async def interaction_check(self, interaction: Interaction) -> bool:
        """Check if interaction should be processed."""
        return True

Buttons

Interactive buttons with various styles and emoji support.

class Button(ui.Item):
    """
    Interactive button component.
    
    Parameters:
    - style: Button style (primary, secondary, success, danger, link)
    - label: Button text label
    - disabled: Whether button is disabled
    - custom_id: Custom ID for the button
    - url: URL for link-style buttons
    - emoji: Button emoji
    - row: Row number (0-4) for button placement
    """
    def __init__(
        self,
        *,
        style: ButtonStyle = ButtonStyle.secondary,
        label: Optional[str] = None,
        disabled: bool = False,
        custom_id: Optional[str] = None,
        url: Optional[str] = None,
        emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
        row: Optional[int] = None
    ): ...
    
    style: ButtonStyle  # Button style
    label: Optional[str]  # Button label
    disabled: bool  # Whether button is disabled
    custom_id: Optional[str]  # Custom ID
    url: Optional[str]  # URL for link buttons
    emoji: Optional[Union[str, Emoji, PartialEmoji]]  # Button emoji
    
    async def callback(self, interaction: Interaction) -> None:
        """Called when button is clicked."""
        pass

class ButtonStyle(Enum):
    """Button style enumeration."""
    primary = 1  # Blurple button
    secondary = 2  # Grey button  
    success = 3  # Green button
    danger = 4  # Red button
    link = 5  # Link button (requires URL)
    
    # Aliases
    blurple = 1
    grey = 2
    gray = 2
    green = 3
    red = 4
    url = 5

def button(
    *,
    label: Optional[str] = None,
    custom_id: Optional[str] = None,
    disabled: bool = False,
    style: ButtonStyle = ButtonStyle.secondary,
    emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
    row: Optional[int] = None
) -> Callable:
    """Decorator to create a button in a View."""

Select Menus

Dropdown menus for choosing from predefined options or Discord entities.

class Select(ui.Item):
    """
    Base select menu component.
    
    Parameters:
    - custom_id: Custom ID for the select menu
    - placeholder: Placeholder text when nothing is selected
    - min_values: Minimum number of selections required
    - max_values: Maximum number of selections allowed
    - disabled: Whether select menu is disabled
    - row: Row number (0-4) for menu placement
    """
    def __init__(
        self,
        *,
        custom_id: Optional[str] = None,
        placeholder: Optional[str] = None,
        min_values: int = 1,
        max_values: int = 1,
        disabled: bool = False,
        row: Optional[int] = None
    ): ...
    
    custom_id: Optional[str]  # Custom ID
    placeholder: Optional[str]  # Placeholder text
    min_values: int  # Minimum selections
    max_values: int  # Maximum selections
    disabled: bool  # Whether menu is disabled
    options: List[SelectOption]  # Menu options (for string select)
    values: List[Any]  # Current selected values
    
    async def callback(self, interaction: Interaction) -> None:
        """Called when selection is made."""
        pass

class UserSelect(Select):
    """
    Select menu for choosing users/members.
    
    Values will be User or Member objects.
    """
    def __init__(self, **kwargs): ...
    
    values: List[Union[User, Member]]  # Selected users

class RoleSelect(Select):
    """
    Select menu for choosing roles.
    
    Values will be Role objects.
    """
    def __init__(self, **kwargs): ...
    
    values: List[Role]  # Selected roles

class MentionableSelect(Select):
    """
    Select menu for choosing users or roles.
    
    Values will be User, Member, or Role objects.
    """
    def __init__(self, **kwargs): ...
    
    values: List[Union[User, Member, Role]]  # Selected mentionables

class ChannelSelect(Select):
    """
    Select menu for choosing channels.
    
    Parameters:
    - channel_types: List of allowed channel types
    
    Values will be GuildChannel objects.
    """
    def __init__(self, *, channel_types: List[ChannelType] = None, **kwargs): ...
    
    channel_types: List[ChannelType]  # Allowed channel types
    values: List[GuildChannel]  # Selected channels

class SelectOption:
    """
    Option for string select menus.
    
    Parameters:
    - label: Option display text
    - value: Option value (returned when selected)
    - description: Optional description text
    - emoji: Optional emoji
    - default: Whether option is selected by default
    """
    def __init__(
        self,
        *,
        label: str,
        value: str,
        description: Optional[str] = None,
        emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
        default: bool = False
    ): ...
    
    label: str  # Display text
    value: str  # Option value
    description: Optional[str]  # Description
    emoji: Optional[Union[str, Emoji, PartialEmoji]]  # Emoji
    default: bool  # Whether selected by default

def select(
    *,
    cls: Type[Select] = Select,
    placeholder: Optional[str] = None,
    custom_id: Optional[str] = None,
    min_values: int = 1,
    max_values: int = 1,
    options: List[SelectOption] = None,
    disabled: bool = False,
    row: Optional[int] = None
) -> Callable:
    """Decorator to create a select menu in a View."""

Text Input

Text input fields for modals to collect user text input.

class TextInput(ui.Item):
    """
    Text input field for modals.
    
    Parameters:
    - label: Input field label
    - style: Input style (short or paragraph)
    - custom_id: Custom ID for the input
    - placeholder: Placeholder text
    - default: Default input value
    - required: Whether input is required
    - min_length: Minimum text length
    - max_length: Maximum text length
    - row: Row number (0-4) for input placement
    """
    def __init__(
        self,
        *,
        label: str,
        style: TextStyle = TextStyle.short,
        custom_id: Optional[str] = None,
        placeholder: Optional[str] = None,
        default: Optional[str] = None,
        required: bool = True,
        min_length: Optional[int] = None,
        max_length: Optional[int] = None,
        row: Optional[int] = None
    ): ...
    
    label: str  # Input label
    style: TextStyle  # Input style
    custom_id: Optional[str]  # Custom ID
    placeholder: Optional[str]  # Placeholder text
    default: Optional[str]  # Default value
    required: bool  # Whether required
    min_length: Optional[int]  # Minimum length
    max_length: Optional[int]  # Maximum length
    value: Optional[str]  # Current input value

class TextStyle(Enum):
    """Text input style enumeration."""
    short = 1  # Single line input
    paragraph = 2  # Multi-line input
    
    # Aliases
    long = 2

Base Item Class

Base class for all UI components with common functionality.

class Item:
    """
    Base class for all UI components.
    """
    def __init__(self): ...
    
    type: ComponentType  # Component type
    custom_id: Optional[str]  # Custom ID
    disabled: bool  # Whether component is disabled
    row: Optional[int]  # Row placement
    width: int  # Component width (for layout)
    view: Optional[View]  # Parent view
    
    @property
    def is_dispatchable(self) -> bool:
        """Whether component can receive interactions."""
    
    @property 
    def is_persistent(self) -> bool:
        """Whether component is persistent across bot restarts."""
    
    def to_component_dict(self) -> Dict[str, Any]:
        """Convert component to Discord API format."""
    
    def is_row_full(self, components: List[Item]) -> bool:
        """Check if adding this component would fill the row."""
    
    def refresh_component(self, component: Component) -> None:
        """Refresh component from Discord data."""

Usage Examples

Basic View with Buttons

import discord
from discord.ext import commands

class ConfirmView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=60.0)
        self.value = None
    
    @discord.ui.button(label='Confirm', style=discord.ButtonStyle.green)
    async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message('Confirmed!', ephemeral=True)
        self.value = True
        self.stop()
    
    @discord.ui.button(label='Cancel', style=discord.ButtonStyle.red)
    async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message('Cancelled!', ephemeral=True)
        self.value = False
        self.stop()

@bot.command()
async def confirm_action(ctx):
    view = ConfirmView()
    message = await ctx.send('Do you want to proceed?', view=view)
    
    # Wait for user interaction
    await view.wait()
    
    if view.value is None:
        await message.edit(content='Timed out.', view=None)
    elif view.value:
        await message.edit(content='Action confirmed!', view=None)
    else:
        await message.edit(content='Action cancelled.', view=None)

Select Menu Example

class RoleSelectView(discord.ui.View):
    def __init__(self, roles):
        super().__init__()
        self.roles = roles
        
        # Add select menu with role options
        options = [
            discord.SelectOption(
                label=role.name,
                description=f"Select the {role.name} role",
                value=str(role.id),
                emoji="🎭"
            )
            for role in roles[:25]  # Discord limits to 25 options
        ]
        
        select = discord.ui.Select(
            placeholder="Choose your role...",
            options=options,
            custom_id="role_select"
        )
        select.callback = self.role_callback
        self.add_item(select)
    
    async def role_callback(self, interaction: discord.Interaction):
        role_id = int(interaction.data['values'][0])
        role = discord.utils.get(self.roles, id=role_id)
        
        if role in interaction.user.roles:
            await interaction.user.remove_roles(role)
            await interaction.response.send_message(f'Removed {role.name} role!', ephemeral=True)
        else:
            await interaction.user.add_roles(role)
            await interaction.response.send_message(f'Added {role.name} role!', ephemeral=True)

@bot.command()
async def role_menu(ctx):
    # Get some roles (filter as needed)
    roles = [role for role in ctx.guild.roles if not role.is_default() and not role.managed]
    
    if not roles:
        await ctx.send('No assignable roles found.')
        return
    
    view = RoleSelectView(roles)
    await ctx.send('Select a role to add/remove:', view=view)

Modal Dialog Example

class FeedbackModal(discord.ui.Modal, title='Feedback Form'):
    def __init__(self):
        super().__init__()
    
    # Short text input
    name = discord.ui.TextInput(
        label='Name',
        placeholder='Your name here...',
        required=False,
        max_length=50
    )
    
    # Long text input
    feedback = discord.ui.TextInput(
        label='Feedback',
        style=discord.TextStyle.paragraph,
        placeholder='Tell us what you think...',
        required=True,
        max_length=1000
    )
    
    # Rating input
    rating = discord.ui.TextInput(
        label='Rating (1-5)',
        placeholder='5',
        required=True,
        min_length=1,
        max_length=1
    )
    
    async def on_submit(self, interaction: discord.Interaction):
        # Validate rating
        try:
            rating_num = int(self.rating.value)
            if not 1 <= rating_num <= 5:
                raise ValueError
        except ValueError:
            await interaction.response.send_message('Rating must be a number from 1 to 5!', ephemeral=True)
            return
        
        # Process feedback
        embed = discord.Embed(title='Feedback Received', color=0x00ff00)
        embed.add_field(name='Name', value=self.name.value or 'Anonymous', inline=False)
        embed.add_field(name='Rating', value='⭐' * rating_num, inline=False)
        embed.add_field(name='Feedback', value=self.feedback.value, inline=False)
        embed.set_footer(text=f'From: {interaction.user}')
        
        # Send to feedback channel (replace with actual channel ID)
        feedback_channel = bot.get_channel(123456789)
        if feedback_channel:
            await feedback_channel.send(embed=embed)
        
        await interaction.response.send_message('Thank you for your feedback!', ephemeral=True)
    
    async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
        await interaction.response.send_message('An error occurred. Please try again.', ephemeral=True)

class FeedbackView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)  # Persistent view
    
    @discord.ui.button(label='Give Feedback', style=discord.ButtonStyle.primary, emoji='📝')
    async def feedback_button(self, interaction: discord.Interaction, button: discord.ui.Button):
        modal = FeedbackModal()
        await interaction.response.send_modal(modal)

@bot.command()
async def feedback(ctx):
    view = FeedbackView()
    embed = discord.Embed(
        title='Feedback System',
        description='Click the button below to give us your feedback!',
        color=0x0099ff
    )
    await ctx.send(embed=embed, view=view)

Advanced Multi-Component View

class MusicControlView(discord.ui.View):
    def __init__(self, voice_client):
        super().__init__(timeout=300.0)
        self.voice_client = voice_client
        self.volume = 50
    
    @discord.ui.button(emoji='⏯️', style=discord.ButtonStyle.primary)
    async def play_pause(self, interaction: discord.Interaction, button: discord.ui.Button):
        if self.voice_client.is_playing():
            self.voice_client.pause()
            await interaction.response.send_message('⏸️ Paused', ephemeral=True)
        elif self.voice_client.is_paused():
            self.voice_client.resume()
            await interaction.response.send_message('▶️ Resumed', ephemeral=True)
        else:
            await interaction.response.send_message('Nothing to play', ephemeral=True)
    
    @discord.ui.button(emoji='⏹️', style=discord.ButtonStyle.secondary)
    async def stop(self, interaction: discord.Interaction, button: discord.ui.Button):
        self.voice_client.stop()
        await interaction.response.send_message('⏹️ Stopped', ephemeral=True)
    
    @discord.ui.button(emoji='⏭️', style=discord.ButtonStyle.secondary)
    async def skip(self, interaction: discord.Interaction, button: discord.ui.Button):
        self.voice_client.stop()  # This will trigger the next song
        await interaction.response.send_message('⏭️ Skipped', ephemeral=True)
    
    @discord.ui.select(
        placeholder="Adjust volume...",
        options=[
            discord.SelectOption(label="🔈 Low (25%)", value="25"),
            discord.SelectOption(label="🔉 Medium (50%)", value="50", default=True),
            discord.SelectOption(label="🔊 High (75%)", value="75"),
            discord.SelectOption(label="📢 Max (100%)", value="100"),
        ]
    )
    async def volume_select(self, interaction: discord.Interaction, select: discord.ui.Select):
        self.volume = int(select.values[0])
        # Apply volume change to audio source if supported
        await interaction.response.send_message(f'🔊 Volume set to {self.volume}%', ephemeral=True)
    
    async def on_timeout(self):
        # Disable all components when view times out
        for item in self.children:
            item.disabled = True
        
        # Update the message to show timeout
        try:
            await self.message.edit(view=self)
        except:
            pass

@bot.command()
async def music_controls(ctx):
    if not ctx.voice_client:
        await ctx.send('Not connected to a voice channel.')
        return
    
    view = MusicControlView(ctx.voice_client)
    embed = discord.Embed(title='🎵 Music Controls', color=0x9932cc)
    message = await ctx.send(embed=embed, view=view)
    view.message = message  # Store message reference for timeout handling

Persistent View (Survives Bot Restart)

class PersistentRoleView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)
    
    @discord.ui.button(
        label='Get Updates Role',
        style=discord.ButtonStyle.primary,
        custom_id='persistent_view:updates_role'  # Must be unique and persistent
    )
    async def updates_role(self, interaction: discord.Interaction, button: discord.ui.Button):
        role = discord.utils.get(interaction.guild.roles, name='Updates')
        
        if not role:
            await interaction.response.send_message('Updates role not found!', ephemeral=True)
            return
        
        if role in interaction.user.roles:
            await interaction.user.remove_roles(role)
            await interaction.response.send_message('Removed Updates role!', ephemeral=True)
        else:
            await interaction.user.add_roles(role)
            await interaction.response.send_message('Added Updates role!', ephemeral=True)

@bot.event
async def on_ready():
    # Re-add persistent views on bot startup
    bot.add_view(PersistentRoleView())
    print(f'{bot.user} is ready!')

@bot.command()
async def setup_roles(ctx):
    view = PersistentRoleView()
    embed = discord.Embed(
        title='Role Assignment',
        description='Click the button to toggle the Updates role.',
        color=0x00ff00
    )
    await ctx.send(embed=embed, view=view)

Install with Tessl CLI

npx tessl i tessl/pypi-discord-py

docs

app-commands.md

commands-framework.md

core-objects.md

event-handling.md

index.md

user-interface.md

utilities.md

voice-audio.md

webhooks.md

tile.json