Component classes build Discord component JSON for you. They take string-style values, not enums.
Button
from yourbot_sdk import Button, ActionRow
Button(
label="Click me",
custom_id="btn_hi",
style="primary", # "primary" | "secondary" | "success" | "danger" | "link"
emoji="🎉",
disabled=False,
)
# Link buttons use url instead of custom_id
Button(label="Docs", url="https://yourbot.gg/dev/docs", style="link")
SelectMenu
from yourbot_sdk import SelectMenu, SelectOption, ActionRow
menu = SelectMenu(
custom_id="pick_role",
options=[
SelectOption(label="Gamer", value="role_gamer", description="Get pinged for game nights"),
SelectOption(label="Music", value="role_music", default=True), # pre-selected
SelectOption(label="Art", value="role_art", emoji="🎨"),
],
placeholder="Pick your interests…",
min_values=1,
max_values=2,
disabled=False,
)
ActionRow
Discord shows components in rows. Wrap one or more components in an ActionRow and pass it as components=[…]. A row holds at most 5 components (extras are silently dropped), and over-long strings on any builder (labels, placeholders, custom_ids) are silently truncated to Discord's caps rather than raising — keep them short by construction:
@plugin.on_slash_command("menu")
def menu_cmd(ctx: Context, event: dict):
row = ActionRow(
Button("Yes", custom_id="yes", style="success"),
Button("No", custom_id="no", style="danger"),
)
ctx.interaction.respond(content="Pick one:", components=[row])
@plugin.on_component("yes")
def yes_clicked(ctx: Context, event: dict):
ctx.interaction.respond(content="✅", ephemeral=True)
@plugin.on_component("no")
def no_clicked(ctx: Context, event: dict):
ctx.interaction.respond(content="❌", ephemeral=True)
Dynamic custom_ids: prefix matching
@plugin.on_component("exact_id") matches the whole string. When your custom_id encodes state (page numbers, vote targets), register with prefix= instead and parse the rest yourself. Exactly one of custom_id= or prefix= must be given.
@plugin.on_component(prefix="page:")
def handle_page(ctx: Context, event: dict):
cid = event["custom_id"] # e.g. "page:next:5"
_, direction, index = cid.split(":", 2)
...
Buttons posted by an older version of your plugin stay clickable on Discord forever. If you encode state in custom_id, embed a schema version too (e.g. "v2:vote:42") so stale buttons can be detected and politely refused.
Updating a message in place v0.7.0
Inside a component handler, pass update_message=True to respond() to edit the message the button or menu is attached to (game boards, pagination, live leaderboards) instead of sending a new reply:
@plugin.on_component(prefix="counter:")
def bump(ctx: Context, event: dict):
count = int(event["custom_id"].split(":")[1]) + 1
ctx.interaction.respond(
content=f"Count: **{count}**",
components=[ActionRow(Button("+1", custom_id=f"counter:{count}"))],
update_message=True,
)
- Component interactions only. The platform rejects it for slash commands and modal submits.
- The fields you pass replace the message's current content/embeds/components, so pass everything the updated message should contain.
ephemeralis ignored (the message keeps its original visibility). You can call it repeatedly within the 15-minute interaction window.