A modern, feature-rich, and async-ready API wrapper for Discord written in Python
—
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.
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 TrueInteractive 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."""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 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 = 2Base 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."""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)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)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)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 handlingclass 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