Concepts
Buttons and menus
Two ways to add buttons and select menus, and how to pick the right one.
There are two ways to make buttons and menus work. Picking the right one saves a lot of code.
| View | Component + event | |
|---|---|---|
| Lives in | The command itself | A separate event |
| Works for | A few minutes after the command | Forever, even after the bot restarts |
| Can see the command's variables | Yes | No |
| Good for | Confirm prompts, games, pickers, quizzes | Role panels, ticket buttons, "click any time" panels |
Rule of thumb: if the click happens soon after the command, use a View. If the button must keep working for days, use a component and an event.
Views#
A View is a group of buttons and menus inside the command. You write what each button does right there, next to the rest of the command.
Pre-hook:
class Confirm(View):
def __init__(self, user_id):
self.user_id = user_id # only this person can click
self.choice = None
@button(label="Yes", style="success", custom_id="yes")
async def yes(self, interaction, component):
await defer(interaction, action="update")
self.choice = "yes"
self.disable_all_components()
await self.update_view()
self.stop()
@button(label="No", style="danger", custom_id="no")
async def no(self, interaction, component):
await defer(interaction, action="update")
self.choice = "no"
self.disable_all_components()
await self.update_view()
self.stop()
view = Confirm(user.id)
view.interaction = interaction
set_view(view)
set_content("Are you sure?")Post-hook:
if view.choice == "yes":
send_message(channel, "Confirmed.")
elif view.choice is None:
send_message(channel, "Timed out.")The post-hook runs after the View stops, or after it times out.
Things to know:
- Start every button with
await defer(interaction, action="update"). It tells Discord the click was received. user_idlimits who can click. Leave it out and anyone can click.self.stop()ends the View.await self.update_view()redraws the buttons, for example after you disable them.- The View times out after 3 minutes with no clicks.
- Never call
super().__init__(). - With
set_view, the bot waits for the View for you. Do not callview.wait().
Full list: Views.
Reading what was picked#
In a menu callback, read the choice from interaction.values. It is always a list.
@text_menu(
options=[TextOption(label="Red", value="red"), TextOption(label="Blue", value="blue")],
custom_id="color",
)
async def color(self, interaction, component):
await defer(interaction, action="update")
self.picked = interaction.values[0]
self.stop()Watch out
component is the menu you wrote, not what the user picked. component.values does not hold the choice. Use interaction.values.
Components and events#
For a button that must work forever, split it in two:
- A command posts the button with a
custom_id. - An event listens for that
custom_idand does the work.
Command pre-hook:
c = ComponentBuilder()
c.add_button("Open ticket", style="primary", custom_id="ticket_open")
set_components(c)
set_content("Need help? Press the button.")Event, type Button Click / Select Menu Change, Custom ids: ticket_open:
defer_response(ephemeral=True)
respond_interaction(interaction, "Your ticket is being made…")The event does not know anything from the command. It only gets custom_id, user, channel, message and interaction.
Full list: Components.
The 3-second rule#
Discord waits 3 seconds for an answer to a click. Without one, the user sees "This interaction failed".
- In a View button, start with
await defer(interaction, action="update"). - In an event, start with
defer_response(...).
Then do the slow work.
See Interactions.
Discord limits#
- At most 5 rows.
- At most 5 buttons in a row.
- A select menu takes a whole row by itself.