discordbot/docs/components.md

10 KiB

discord.ui -- User Interface Components

Discord's UI kit provides ways to build interactive interfaces using components like buttons, select menus, and modals. The discord.ui{.interpreted-text role="mod"} module in this library offers high level abstractions to create these interfaces easily.

This page presents a basic overview of how to build views, lay out components, and respond to user interaction.

Creating a View

A ~discord.ui.View{.interpreted-text role="class"} groups components together and manages the interaction lifecycle. Views can be persistent or temporary and support custom timeouts.

from discord.ext import commands
import discord

bot = commands.Bot(command_prefix="!")

class Confirm(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=30)
        self.value = None

    @discord.ui.button(label="Confirm", style=discord.ButtonStyle.green)
    async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
        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):
        self.value = False
        self.stop()

@bot.command()
async def ask(ctx: commands.Context):
    view = Confirm()
    await ctx.send("Do you wish to continue?", view=view)
    await view.wait()
    if view.value is None:
        await ctx.send("Timed out...")
    elif view.value:
        await ctx.send("Confirmed!")
    else:
        await ctx.send("Cancelled.")

Handling Select Menus

Select menus allow members to choose from a list of options. They are represented by ~discord.ui.Select{.interpreted-text role="class"} and can be defined inside a view with the ~discord.ui.select{.interpreted-text role="func"} decorator.

class FruitView(discord.ui.View):
    @discord.ui.select(
        placeholder="Choose your favourite fruit",
        min_values=1,
        max_values=1,
        options=[
            discord.SelectOption(label="Apple", value="apple"),
            discord.SelectOption(label="Banana", value="banana"),
            discord.SelectOption(label="Orange", value="orange"),
        ],
    )
    async def select_callback(self, interaction: discord.Interaction, select: discord.ui.Select):
        await interaction.response.send_message(f"You chose {select.values[0]}!")

Buttons and Modals

Buttons are created with ~discord.ui.Button{.interpreted-text role="class"} and can be declared using the ~discord.ui.button{.interpreted-text role="func"} decorator. For more complex input, ~discord.ui.Modal{.interpreted-text role="class"} provides a dialog style interface with text inputs.

class Feedback(discord.ui.Modal, title="Feedback"):
    name = discord.ui.TextInput(label="Name")
    message = discord.ui.TextInput(label="Feedback", style=discord.TextStyle.paragraph)

    async def on_submit(self, interaction: discord.Interaction):
        await interaction.response.send_message("Thanks for your feedback!", ephemeral=True)

class FeedbackView(discord.ui.View):
    @discord.ui.button(label="Give Feedback", style=discord.ButtonStyle.primary)
    async def feedback(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_modal(Feedback())

Combining Everything

Views can contain multiple components arranged in rows. Each view keeps track of its children and automatically disables components when timed out.

More information about the available classes and their APIs can be found in the interactions/api{.interpreted-text role="doc"} reference under discord_ui_kit{.interpreted-text role="ref"}.

Persistent Views

Sometimes a view should remain active even after the bot restarts. To achieve this, register the view with discord.Client.add_view{.interpreted-text role="meth"} and ensure all components provide a custom_id. Persistent views cannot have a timeout set.

class Poll(discord.ui.View):
    def __init__(self):
        super().__init__(timeout=None)

    @discord.ui.button(label="Vote", style=discord.ButtonStyle.primary,
                       custom_id="poll:vote")
    async def vote(self, interaction: discord.Interaction,
                   button: discord.ui.Button):
        await interaction.response.send_message(
            "Thanks for voting!", ephemeral=True
        )

bot.add_view(Poll())

Timeouts and Disabling Components

When a view times out you can disable the components to prevent further interaction. Override ~discord.ui.View.on_timeout{.interpreted-text role="meth"} and edit the message that contains the view to update it.

class AutoDisable(discord.ui.View):
    async def on_timeout(self) -> None:
        for item in self.children:
            item.disabled = True

        await self.message.edit(view=self)

Component Layout and Row Management

Each message can contain up to five rows of components with five components in each row. By default, items are appended in the order they are defined but you can explicitly control the layout using the row keyword argument or by manually adding items with ~discord.ui.View.add_item{.interpreted-text role="meth"}.

class RowExample(discord.ui.View):
    def __init__(self):
        super().__init__()
        self.add_item(discord.ui.Button(label="Top", row=0))
        self.add_item(discord.ui.Button(label="Bottom", row=4))

    @discord.ui.button(label="Middle", row=2)
    async def middle(self, interaction: discord.Interaction, _: discord.ui.Button):
        await interaction.response.send_message("Middle row button")

Handling Interaction Responses

Interactions need a response within three seconds. If your callback requires long processing you should defer the response to buy yourself more time.

class DeferExample(discord.ui.View):
    @discord.ui.button(label="Process", style=discord.ButtonStyle.blurple)
    async def process(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.defer(thinking=True)
        await lengthy_operation()
        await interaction.followup.send("Done!", ephemeral=True)

Pre-defined Select Menus

In addition to ~discord.ui.Select{.interpreted-text role="class"}, there are convenience subclasses for common types of selections such as channels, roles, users, and mentionables. These can be constructed manually or by passing the cls keyword parameter to the ~discord.ui.select{.interpreted-text role="func"} decorator.

class SelectView(discord.ui.View):
    @discord.ui.select(cls=discord.ui.ChannelSelect, channel_types=[discord.ChannelType.voice])
    async def channel(self, interaction: discord.Interaction, select: discord.ui.ChannelSelect):
        await interaction.response.send_message(f"Chosen channel: {select.values[0]}")

Pagination Example

The following example demonstrates how the building blocks come together to create a paginated message. Clicking the buttons will edit the message in-place to show the next or previous page.

class Paginator(discord.ui.View):
    def __init__(self, pages: list[str]):
        super().__init__(timeout=60)
        self.pages = pages
        self.index = 0

    async def update_message(self, interaction: discord.Interaction):
        content = self.pages[self.index]
        await interaction.response.edit_message(content=content, view=self)

    @discord.ui.button(label="Prev", style=discord.ButtonStyle.secondary)
    async def previous(self, interaction: discord.Interaction, _button: discord.ui.Button):
        self.index = (self.index - 1) % len(self.pages)
        await self.update_message(interaction)

    @discord.ui.button(label="Next", style=discord.ButtonStyle.secondary)
    async def next(self, interaction: discord.Interaction, _button: discord.ui.Button):
        self.index = (self.index + 1) % len(self.pages)
        await self.update_message(interaction)

@bot.command()
async def paginate(ctx: commands.Context):
    view = Paginator(["Page 1", "Page 2", "Page 3"])
    await ctx.send("Page 1", view=view)

Advanced Components

The UI kit also provides a number of layout components that only work inside of a ~discord.ui.LayoutView{.interpreted-text role="class"}. These components allow building complex message layouts with embedded media and rich formatting.

Action Rows and Layout Views

An ~discord.ui.ActionRow{.interpreted-text role="class"} groups interactive items within a layout. Use it inside a ~discord.ui.LayoutView{.interpreted-text role="class"} to position buttons or selects.

class ActionView(discord.ui.LayoutView):
    row = discord.ui.ActionRow()

    @row.button(label="Click Me")
    async def click_me(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message("You clicked the row button!")

Containers

Containers group other layout components together and can be styled with an accent colour.

class ContainerView(discord.ui.LayoutView):
    container = discord.ui.Container(
        discord.ui.TextDisplay("Hello from a container!"),
        discord.ui.ActionRow(discord.ui.Button(label="OK")),
    )

Files and Media Galleries

Use ~discord.ui.File{.interpreted-text role="class"} to reference attachments uploaded alongside the view. ~discord.ui.MediaGallery{.interpreted-text role="class"} displays a collection of images or videos.

class GalleryView(discord.ui.LayoutView):
    file = discord.ui.File("attachment://local.png")

    def __init__(self):
        super().__init__()
        self.gallery = discord.ui.MediaGallery()
        self.gallery.add_item("https://example.com/image.png", description="Example")

Sections, Text Displays and Thumbnails

Sections organise text with an optional accessory like a thumbnail. Additional static text can be shown with ~discord.ui.TextDisplay{.interpreted-text role="class"} while ~discord.ui.Separator{.interpreted-text role="class"} provides spacing between elements.

class InfoView(discord.ui.LayoutView):
    section = discord.ui.Section(
        "Important information",
        accessory=discord.ui.Thumbnail("https://example.com/thumb.png"),
    )

    separator = discord.ui.Separator()
    extra = discord.ui.TextDisplay("More details here")

See Also

More examples can be found in the examples <examples>{.interpreted-text role="resource"} directory. Refer to the interactions/api{.interpreted-text role="doc"} page for an exhaustive API reference.