Skip to main content
UI components allow you to create interactive experiences in Discord with buttons, select menus, and modals.

Buttons

Buttons are clickable components that can trigger actions:

Basic Button

import discord

class MyView(discord.ui.View):
    @discord.ui.button(label="Click Me!", style=discord.ButtonStyle.primary)
    async def button_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Button clicked!", ephemeral=True)

@bot.slash_command()
async def buttons(ctx: discord.ApplicationContext):
    view = MyView()
    await ctx.respond("Press the button!", view=view)

Button Styles

class StyledButtons(discord.ui.View):
    @discord.ui.button(label="Primary", style=discord.ButtonStyle.primary)
    async def primary(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Primary!", ephemeral=True)

    @discord.ui.button(label="Secondary", style=discord.ButtonStyle.secondary)
    async def secondary(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Secondary!", ephemeral=True)

    @discord.ui.button(label="Success", style=discord.ButtonStyle.success)
    async def success(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Success!", ephemeral=True)

    @discord.ui.button(label="Danger", style=discord.ButtonStyle.danger)
    async def danger(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Danger!", ephemeral=True)

Button with Emoji

@discord.ui.button(label="Like", emoji="👍", style=discord.ButtonStyle.success)
async def like_button(self, button: discord.ui.Button, interaction: discord.Interaction):
    await interaction.response.send_message("Thanks for the like!", ephemeral=True)
class LinkView(discord.ui.View):
    def __init__(self):
        super().__init__()
        # Link buttons don't have callbacks
        self.add_item(discord.ui.Button(
            label="Visit Pycord",
            url="https://pycord.dev",
            style=discord.ButtonStyle.link
        ))

Counter Example

Create a button that tracks state:
class Counter(discord.ui.View):
    @discord.ui.button(label="0", style=discord.ButtonStyle.red)
    async def count(self, button: discord.ui.Button, interaction: discord.Interaction):
        number = int(button.label) if button.label else 0
        if number >= 4:
            button.style = discord.ButtonStyle.green
            button.disabled = True
        button.label = str(number + 1)
        
        # Update the message with the modified view
        await interaction.response.edit_message(view=self)

Disabling Buttons

@discord.ui.button(label="Disabled", style=discord.ButtonStyle.secondary, disabled=True)
async def disabled_button(self, button: discord.ui.Button, interaction: discord.Interaction):
    # This will never be called
    pass

Select Menus

Select menus (dropdowns) let users choose from multiple options:

String Select

class Dropdown(discord.ui.Select):
    def __init__(self):
        options = [
            discord.SelectOption(
                label="Red",
                description="Your favorite color is red",
                emoji="🟥"
            ),
            discord.SelectOption(
                label="Green",
                description="Your favorite color is green",
                emoji="🟩"
            ),
            discord.SelectOption(
                label="Blue",
                description="Your favorite color is blue",
                emoji="🟦"
            ),
        ]
        
        super().__init__(
            placeholder="Choose your favorite color...",
            min_values=1,
            max_values=1,
            options=options,
        )
    
    async def callback(self, interaction: discord.Interaction):
        await interaction.response.send_message(
            f"Your favorite color is {self.values[0]}",
            ephemeral=True
        )

class DropdownView(discord.ui.View):
    def __init__(self):
        super().__init__()
        self.add_item(Dropdown())

@bot.slash_command()
async def color(ctx: discord.ApplicationContext):
    view = DropdownView()
    await ctx.respond("Pick your favorite color:", view=view)

User Select

class UserSelect(discord.ui.View):
    @discord.ui.select(
        select_type=discord.ComponentType.user_select,
        placeholder="Select a user",
        min_values=1,
        max_values=1,
    )
    async def user_select_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
        user = select.values[0]
        await interaction.response.send_message(
            f"You selected {user.mention}",
            ephemeral=True
        )

Role Select

class RoleSelect(discord.ui.View):
    @discord.ui.select(
        select_type=discord.ComponentType.role_select,
        placeholder="Select a role",
        min_values=1,
        max_values=3,  # Allow up to 3 roles
    )
    async def role_select_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
        roles = ", ".join([role.mention for role in select.values])
        await interaction.response.send_message(
            f"Selected roles: {roles}",
            ephemeral=True
        )

Channel Select

class ChannelSelect(discord.ui.View):
    @discord.ui.select(
        select_type=discord.ComponentType.channel_select,
        placeholder="Select a channel",
        channel_types=[discord.ChannelType.text, discord.ChannelType.voice],
    )
    async def channel_select_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
        channel = select.values[0]
        await interaction.response.send_message(
            f"You selected {channel.mention}",
            ephemeral=True
        )

Mentionable Select

class MentionableSelect(discord.ui.View):
    @discord.ui.select(
        select_type=discord.ComponentType.mentionable_select,
        placeholder="Select users or roles",
        min_values=1,
        max_values=5,
    )
    async def mentionable_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
        mentions = ", ".join([item.mention for item in select.values])
        await interaction.response.send_message(
            f"Selected: {mentions}",
            ephemeral=True
        )

Modals

Modals are pop-up forms that collect user input:

Basic Modal

class MyModal(discord.ui.DesignerModal):
    def __init__(self, *args, **kwargs):
        first_input = discord.ui.Label(
            "Short Input",
            discord.ui.InputText(
                placeholder="Type something...",
            ),
        )
        second_input = discord.ui.Label(
            "Long Input",
            discord.ui.InputText(
                placeholder="Type a longer response...",
                style=discord.InputTextStyle.long,
            ),
            description="This input accepts longer text.",
        )
        
        super().__init__(first_input, second_input, *args, **kwargs)
    
    async def callback(self, interaction: discord.Interaction):
        # Access input values
        short_input = self.children[0].item.value
        long_input = self.children[1].item.value
        
        await interaction.response.send_message(
            f"Short: {short_input}\nLong: {long_input}",
            ephemeral=True
        )

@bot.slash_command()
async def form(ctx: discord.ApplicationContext):
    modal = MyModal(title="Feedback Form")
    await ctx.send_modal(modal)
class AdvancedModal(discord.ui.DesignerModal):
    def __init__(self, *args, **kwargs):
        name_input = discord.ui.Label(
            "Your Name",
            discord.ui.InputText(placeholder="Enter your name"),
        )
        
        color_select = discord.ui.Label(
            "Favorite Color",
            discord.ui.Select(
                placeholder="Select a color",
                options=[
                    discord.SelectOption(label="Red", emoji="🟥"),
                    discord.SelectOption(label="Green", emoji="🟩"),
                    discord.SelectOption(label="Blue", emoji="🟦"),
                ],
                required=False,
            ),
            description="Optional: Select your favorite color",
        )
        
        super().__init__(name_input, color_select, *args, **kwargs)
    
    async def callback(self, interaction: discord.Interaction):
        name = self.children[0].item.value
        color = self.children[1].item.values[0] if self.children[1].item.values else "None"
        
        await interaction.response.send_message(
            f"Name: {name}\nColor: {color}",
            ephemeral=True
        )
class ModalView(discord.ui.View):
    @discord.ui.button(label="Open Form", style=discord.ButtonStyle.primary)
    async def button_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
        modal = MyModal(title="Button Modal")
        await interaction.response.send_modal(modal)

View Management

Timeout

class TimedView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=60)  # 60 seconds
    
    async def on_timeout(self):
        # Called when view times out
        for item in self.children:
            item.disabled = True
        # Update the message to disable all components

Persistent Views

Views that persist across bot restarts:
class PersistentView(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)  # No timeout
    
    @discord.ui.button(
        label="Persistent Button",
        style=discord.ButtonStyle.success,
        custom_id="persistent_button",  # Must be set for persistent views
    )
    async def persistent_button(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Still working!", ephemeral=True)

@bot.event
async def on_ready():
    # Re-register persistent view on startup
    bot.add_view(PersistentView())

Disable on Use

class OneTimeButton(discord.ui.View):
    @discord.ui.button(label="Use Once", style=discord.ButtonStyle.primary)
    async def one_time(self, button: discord.ui.Button, interaction: discord.Interaction):
        button.disabled = True
        await interaction.response.edit_message(view=self)
        await interaction.followup.send("Button disabled!", ephemeral=True)

Stop View

class StoppableView(discord.ui.View):
    @discord.ui.button(label="Action", style=discord.ButtonStyle.primary)
    async def action(self, button: discord.ui.Button, interaction: discord.Interaction):
        await interaction.response.send_message("Action performed!", ephemeral=True)
    
    @discord.ui.button(label="Close", style=discord.ButtonStyle.danger)
    async def close(self, button: discord.ui.Button, interaction: discord.Interaction):
        self.stop()  # Stop listening for interactions
        await interaction.response.edit_message(view=None)  # Remove components

Advanced Patterns

Paginator

class Paginator(discord.ui.View):
    def __init__(self, pages: list[discord.Embed]):
        super().__init__()
        self.pages = pages
        self.current_page = 0
    
    @discord.ui.button(emoji="⬅️", style=discord.ButtonStyle.secondary)
    async def previous(self, button: discord.ui.Button, interaction: discord.Interaction):
        self.current_page = (self.current_page - 1) % len(self.pages)
        await interaction.response.edit_message(embed=self.pages[self.current_page])
    
    @discord.ui.button(emoji="➡️", style=discord.ButtonStyle.secondary)
    async def next(self, button: discord.ui.Button, interaction: discord.Interaction):
        self.current_page = (self.current_page + 1) % len(self.pages)
        await interaction.response.edit_message(embed=self.pages[self.current_page])

Confirmation Dialog

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

@bot.slash_command()
async def delete(ctx: discord.ApplicationContext):
    view = Confirm()
    await ctx.respond("Are you sure you want to delete?", view=view)
    await view.wait()
    
    if view.value:
        await ctx.send("Deleted!")

Role Assignment

class RoleButton(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)
    
    @discord.ui.button(
        label="Get Member Role",
        style=discord.ButtonStyle.success,
        custom_id="member_role",
    )
    async def member_role(self, button: discord.ui.Button, interaction: discord.Interaction):
        role = discord.utils.get(interaction.guild.roles, name="Member")
        if role in interaction.user.roles:
            await interaction.user.remove_roles(role)
            await interaction.response.send_message("Role removed!", ephemeral=True)
        else:
            await interaction.user.add_roles(role)
            await interaction.response.send_message("Role added!", ephemeral=True)

Best Practices

1
Use ephemeral responses
2
For personal information, use ephemeral=True to hide responses from others.
3
Set appropriate timeouts
4
Views should have timeouts unless they’re persistent.
5
Disable components when done
6
Disable buttons/selects after use or on timeout to prevent confusion.
7
Validate user input
8
Always validate data from modals and selects before using it.
9
Handle permissions
10
Check if the bot has permissions before performing actions.
Component Limits:
  • Maximum 5 action rows per message
  • Maximum 5 buttons per row
  • Only 1 select menu per row
  • Maximum 25 options per select menu
  • Button labels must be 80 characters or fewer
  • Custom IDs must be 100 characters or fewer

See Also

Build docs developers (and LLMs) love