Discord bot messages that display long lists of items such as server members, search results, or leaderboards become unreadable when they exceed a few lines. Pagination splits this content into smaller pages that users can flip through using interactive buttons. Discord’s Button Components provide a clean, native way to add forward, backward, and page-number buttons directly below your embed or message. This article explains how to build a pagination system using Discord’s Button Components in a Python bot with discord.py, covering the required imports, button creation, and pagination logic.
Key Takeaways: Discord Button Pagination with discord.py
- discord.ui.View and discord.ui.Button: Core classes for creating interactive button rows in embeds or messages.
- Custom callback functions: Each button must have a callback that updates the embed content and page index.
- Page data stored in a list: Organize your content into a list of strings or embeds where each index represents one page.
Understanding Discord Button Components for Pagination
Discord’s Button Components are part of the Interaction framework introduced in API v8. Unlike the old reaction-based pagination, buttons are persistent, do not require the bot to wait for reactions, and can be disabled when the user reaches the first or last page. The discord.ui.View class is used to hold one or more buttons. Each button is an instance of discord.ui.Button with a style parameter that controls its color: discord.ButtonStyle.primary for blue, discord.ButtonStyle.secondary for gray, discord.ButtonStyle.success for green, discord.ButtonStyle.danger for red, and discord.ButtonStyle.link for URL buttons.
To implement pagination, you need a way to track which page the user is currently viewing. This is typically done by storing the page index as an attribute of the View class. When a button is clicked, the callback method updates the embed content to show the data for the new page index. The button row can include up to five buttons per row, so a common layout uses three buttons: a “Previous” button, a “Page X of Y” label button that is disabled, and a “Next” button. The label button can be replaced with a simple text display or omitted if you prefer two buttons.
Before writing code, ensure your bot has the message_content intent enabled if you plan to read message content, and the application_commands scope if using slash commands. The example in this article uses a slash command to trigger the paginated message, but the same View can be attached to any message sent by the bot.
Steps to Implement Discord Bot Pagination with Buttons
The following steps assume you have a working Discord bot with discord.py version 2.0 or higher installed. If you are using an older version, upgrade with pip install -U discord.py. The example creates a paginated list of 20 items, displaying 5 items per page for a total of 4 pages.
- Import required modules
Start your script by importing discord and the necessary classes:import discordandfrom discord.ext import commands. You also needdiscord.ui.View,discord.ui.Button, anddiscord.Embed. - Define a custom View class
Create a class that inherits fromdiscord.ui.View. Inside the__init__method, accept parameters for the list of pages and optionally the author’s user ID to restrict button interactions. Setself.pages = pages,self.current_page = 0, and callsuper().__init__(). Then add the buttons usingself.add_item(). - Create the button callbacks
Define two async methods decorated with@discord.ui.button. The first method handles the “Previous” button. It checks ifself.current_page > 0, decrements the page index, and callsself.update_embed(interaction). The second method handles the “Next” button, increments the page index ifself.current_page < len(self.pages) - 1, and calls the same update method. - Write the embed update method
Create an async method namedupdate_embedthat takes aninteractionparameter. Inside, build a newdiscord.Embedusingself.pages[self.current_page]as the description. Optionally set the footer to show "Page X of Y". Then callawait interaction.response.edit_message(embed=new_embed, view=self)to update the original message. - Disable buttons at boundaries
In theupdate_embedmethod, after building the embed, loop throughself.childrenand disable the Previous button whenself.current_page == 0and disable the Next button whenself.current_page == len(self.pages) - 1. This prevents users from clicking buttons that have no effect. - Create a slash command to trigger pagination
Define a slash command group or a simple command that generates the page list. For example, a list of 20 strings:pages = [f"Item {i+1}" for i in range(20)]. Then chunk the list into groups of 5:page_texts = ['\n'.join(pages[i:i+5]) for i in range(0, len(pages), 5)]. Send the first page as an embed and pass the View instance with all pages. - Restrict button interactions to the command author
In each button callback, checkif interaction.user.id != self.author_id: return. This prevents other users from flipping pages on a message they did not initiate. Storeself.author_idin the View's__init__method. - Handle edge cases
If the page list is empty, send a message saying no data is available. If there is only one page, disable both buttons from the start. You can also add a timeout to the View by passingtimeout=180to the parent__init__to stop listening after 3 minutes.
Common Mistakes and Limitations
Buttons stop working after the first click
This usually happens because the View is not passed back in the edit_message call. Always include view=self in the edit_message method. If you forget, Discord removes the button components from the message after the first interaction.
Multiple users can click the same buttons
By default, any user can click buttons on a message. To restrict interaction to the original command author, store the author's user ID in the View and check it in every callback. If the check fails, either ignore the interaction or respond with an ephemeral message saying "You cannot control this pagination."
Page content does not update visually
Ensure you are editing the original message, not sending a new one. Use interaction.response.edit_message() or interaction.edit_original_response(). If you use interaction.response.send_message(), a new message appears instead of updating the current one.
Buttons appear but are unclickable
This can occur if the bot does not have the send_messages permission in the channel or if the message was sent before the bot was ready. Also verify that the View's timeout parameter is not set to 0, which would immediately disable all buttons.
Pagination Button Styles: Default vs Custom
| Item | Default Button Style | Custom Button Style |
|---|---|---|
| Button appearance | Uses discord.ButtonStyle.primary (blue) for both Previous and Next |
Uses discord.ButtonStyle.secondary (gray) or discord.ButtonStyle.success (green) for Next |
| Label text | Simple arrows like ◀ and ▶ or words "Previous" and "Next" | Custom emoji or Unicode characters, e.g., ⬅️ and ➡️ |
| Disabled state | Buttons are grayed out when at boundary pages | Same behavior, but you can also change the label to "Start" or "End" when disabled |
| Implementation complexity | Low — just two buttons with basic callbacks | Medium — requires checking page index to update labels dynamically |
The button style does not affect functionality. Choose a style that matches your bot's theme. For accessibility, ensure the button labels clearly indicate direction, such as "Previous" and "Next" rather than ambiguous symbols.
You now have a working pagination system using Discord Button Components. Start by testing with a small data set to confirm the buttons update the embed correctly. Next, try adding a third button that jumps to a specific page by typing a number. For advanced use, store the View in a persistent dictionary keyed by message ID so pagination survives bot restarts.