This uses discord.py 2.6 or newer, where everything here lives in discord.ui. Each example is answered from a slash command; the slash commands guide covers setting those up.
A message with a button
A View is the set of buttons and menus under one message. Each button is a method with the @discord.ui.button decorator, and the method runs when someone presses it:
import discord
class Confirm(discord.ui.View):
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.success)
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message("Confirmed.", ephemeral=True)
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary)
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message("Cancelled.", ephemeral=True)
@bot.tree.command(description="Ask before doing something")
async def ask(interaction: discord.Interaction):
await interaction.response.send_message("Are you sure?", view=Confirm())The callback takes interaction and then the button, in that order. Every press must be answered within three seconds, with send_message, edit_message or defer; otherwise the user sees "This interaction failed". ephemeral=True shows the reply only to the person who pressed.
Styles, links and rows
- Styles:
primary(blue),secondary(grey),success(green) anddanger(red). - Link buttons open a web page and never reach your bot, so they have no callback. Add them with
add_item:self.add_item(discord.ui.Button(label="Docs", url="https://example.com"))in the View's__init__, after callingsuper().__init__(). - Layout: up to five buttons in a row and five rows under a message. Pass
row=1(rows count from 0) to put a button on a row of its own.
Changing the message after a press
Usually a press should update the message it came from, for example to disable the buttons so nobody presses twice. edit_message answers the interaction and edits the message in one go:
class Confirm(discord.ui.View):
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.success)
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
for item in self.children:
item.disabled = True
await interaction.response.edit_message(content="Done.", view=self)
self.stop()self.stop() tells discord.py to stop listening for this View's buttons.
Only the person who asked
Anyone who can see a message can press its buttons. To let only the person who ran the command use them, check in interaction_check, which runs before every callback:
class Confirm(discord.ui.View):
def __init__(self, owner: discord.abc.User):
super().__init__()
self.owner = owner
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.owner.id:
await interaction.response.send_message("This is not your menu.", ephemeral=True)
return False
return True
# in the command: view=Confirm(interaction.user)Timeouts
A View stops listening after 180 seconds without a press. After that its buttons still show, and pressing one gives "This interaction failed". Set your own time with super().__init__(timeout=60), or timeout=None for none, and grey the buttons out when it runs out:
class Confirm(discord.ui.View):
message: discord.Message | None = None
async def on_timeout(self):
for item in self.children:
item.disabled = True
if self.message:
await self.message.edit(view=self)
@bot.tree.command(description="Ask before doing something")
async def ask(interaction: discord.Interaction):
view = Confirm(timeout=60)
await interaction.response.send_message("Are you sure?", view=view)
view.message = await interaction.original_response()A View also forgets its buttons when the bot restarts, whatever the timeout. For buttons that must keep working, see the last section.
Select menus
A select menu offers a list, up to 25 options, and the callback gets what was chosen in select.values:
class Colours(discord.ui.View):
@discord.ui.select(
placeholder="Pick a colour",
options=[
discord.SelectOption(label="Red", value="red", emoji="🟥"),
discord.SelectOption(label="Blue", value="blue", emoji="🟦"),
],
)
async def pick(self, interaction: discord.Interaction, select: discord.ui.Select):
await interaction.response.send_message(f"You picked {select.values[0]}", ephemeral=True)min_values and max_values let people pick several. To pick members, roles or channels from the server rather than from your own list, give the decorator a different class; Discord fills in the choices:
@discord.ui.select(cls=discord.ui.RoleSelect, placeholder="Pick up to three roles", max_values=3)
async def roles(self, interaction: discord.Interaction, select: discord.ui.RoleSelect):
names = ", ".join(role.name for role in select.values)
await interaction.response.send_message(f"You picked {names}", ephemeral=True)The others are discord.ui.UserSelect, discord.ui.ChannelSelect and discord.ui.MentionableSelect. For those, select.values holds the members, roles or channels themselves.
Modals: a pop-up form
A modal is a form that opens over Discord. Each field is a Label with a TextInput (or a select menu) inside it:
class Feedback(discord.ui.Modal, title="Send feedback"):
name = discord.ui.Label(text="Your name", component=discord.ui.TextInput(max_length=50))
text = discord.ui.Label(
text="What could be better?",
description="Up to 1000 characters",
component=discord.ui.TextInput(style=discord.TextStyle.paragraph, max_length=1000),
)
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.send_message(f"Thanks, {self.name.component.value}.", ephemeral=True)
@bot.tree.command(description="Tell us what you think")
async def feedback(interaction: discord.Interaction):
await interaction.response.send_modal(Feedback())Opening a modal has to be the first answer to an interaction: a slash command or a button press can open one, but not after defer or another reply, and a bot cannot open one on its own. A modal holds up to five fields. Each field's answer is in .component.value.
Older code passes label= straight to TextInput. That still runs, but it is deprecated since discord.py 2.6.
Buttons that survive a restart
A role picker or a "open a ticket" button stays in a channel for months, through every restart. For discord.py to recognise its buttons after a restart, three things are needed: no timeout, a fixed custom_id on every item, and registering the View when the bot starts.
class TicketPanel(discord.ui.View):
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(label="Open a ticket", style=discord.ButtonStyle.primary, custom_id="ticketpanel:open")
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message("Opening a ticket...", ephemeral=True)
class MyBot(commands.Bot):
async def setup_hook(self):
self.add_view(TicketPanel()) # listen for its buttons on every message that has them
await self.tree.sync()Post the panel once (from an admin-only command, say), and from then on every restart picks it up again. A custom_id can be up to 100 characters; prefixing it with the View's name keeps two Views from claiming the same press. The button press still needs a running bot, so this only helps if the bot is up: on a host that restarts it after a crash, it is.
Role buttons are the classic use: the reaction roles guide has a whole one, in both libraries. The ticket system guide builds this panel out into private ticket channels.
Common errors
- "This interaction failed": nothing answered within three seconds, the callback raised an error (it is in your console), the View timed out, or the bot restarted and the View was not persistent. For slow work,
await interaction.response.defer()first, thenawait interaction.followup.send(...). InteractionResponded("This interaction has already been responded to"): the code answered twice. The second message goes throughinteraction.followup.send.- The buttons do nothing after a restart: the View is not persistent: give it
timeout=None, acustom_idon every item, andadd_viewinsetup_hook. - "Invalid Form Body" naming a
custom_id: two items in one message share acustom_id, or one is longer than 100 characters. - A modal will not open: the interaction was already answered or deferred.
send_modalhas to come first.
Did this page help?
Thanks for the feedback.