DWEEB
Developer guide · discord.py

Components V2 in discord.py

discord.py sends Components V2 through a LayoutView instead of a View. This guide maps each Discord component to its ui class, shows complete working layouts, and covers what changes compared with the embeds and views you already know.

LayoutView instead of View

discord.py added Components V2 in version 2.6 with a new class, discord.ui.LayoutView. It differs from the View you already use in one important way: a View arranges buttons and menus into rows for you, while a LayoutView holds the whole message layout and leaves the arrangement to you. Text, images, separators and containers all become items in the view.

You send it the usual way — await channel.send(view=view) — and the library sets the Components V2 flag for you. The flag changes Discord's rules for that message: content, embeds, polls and stickers are not accepted alongside it, so visible text goes into TextDisplay items. A layout may hold 40 components in total, nested ones included, and discord.py raises a ValueError as soon as you add one too many.

Every component and its ui class

Put buttons and menus inside a ui.ActionRow yourself. A LayoutView accepts a bare button without complaint, but it serialises as a top-level button, which Discord refuses — in a LayoutView the row is part of your layout, not something the library infers.

Every component and its ui class
Component (type)discord.py classNotes
Container (17)ui.ContainerChildren are positional arguments; accent_colour and spoiler are keywords
Text Display (10)ui.TextDisplayMarkdown text. The only place text can live
Section (9)ui.SectionOne to three text items plus a required accessory= keyword
Thumbnail (11)ui.ThumbnailA section's accessory
Media Gallery (12)ui.MediaGallery with discord.MediaGalleryItemOne to 10 items
Separator (14)ui.Separatorvisible= draws the line; spacing= takes discord.SeparatorSpacing
File (13)ui.FileAn attachment://name reference to an uploaded discord.File
Action Row (1)ui.ActionRowUp to 5 buttons, or one select
Button (2)ui.ButtonIn an action row, or as a section's accessory
Select menus (3, 5–8)ui.Select, ui.UserSelect, ui.RoleSelect, ui.MentionableSelect, ui.ChannelSelectOne per action row

A complete Container message

An accent-striped container with a heading, a divider and a link button — the card most bots start with, and the same message as the minimal JSON payload in the Components V2 reference. Long text is written as adjacent string literals, which Python joins at compile time, so each line of the message stays on its own line of source.

# Components V2 message — discord.py 2.6 or newer.
# Designed in DWEEB (https://dweeb.faizo.net). Edit the text, then send.
import discord
from discord import ui


def build_view() -> ui.LayoutView:
    view = ui.LayoutView(timeout=None)
    view.add_item(
        ui.Container(
            ui.TextDisplay(
                "# Server update\n"
                "Everything you need in one place.",
            ),
            ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small),
            ui.ActionRow(
                ui.Button(
                    style=discord.ButtonStyle.link,
                    label="Read the guide",
                    url="https://example.com/update",
                ),
            ),
            accent_colour=0x5865f2,
        ),
    )
    return view


# Send it from any coroutine: a command, an event listener, a task.
await channel.send(view=build_view())

A section with a thumbnail, and buttons that do something

A Section places text beside one accessory: a thumbnail here, or a single button. The two buttons underneath carry a custom_id, so a click reaches your bot as an interaction and does nothing until you respond to it.

The usual way to handle them is to subclass ui.LayoutView and attach callbacks to the items, as the library's own examples do; the generated function below builds the same tree with add_item so it can be dropped into any cog. For buttons that must keep working after a restart, keep timeout=None, give every interactive item a custom_id, and register the view with bot.add_view() at startup.

# Components V2 message — discord.py 2.6 or newer.
# Designed in DWEEB (https://dweeb.faizo.net). Edit the text, then send.
import discord
from discord import ui


def build_view() -> ui.LayoutView:
    view = ui.LayoutView(timeout=None)
    view.add_item(
        ui.Container(
            ui.Section(
                ui.TextDisplay("## Friday game night"),
                ui.TextDisplay("Doors open at 8 PM. Bring a friend and a headset."),
                accessory=ui.Thumbnail(
                    "https://example.com/game-night.png",
                    description="Game night artwork",
                ),
            ),
            ui.ActionRow(
                ui.Button(
                    style=discord.ButtonStyle.success,
                    label="I'm in",
                    custom_id="rsvp:yes",
                ),
                ui.Button(
                    style=discord.ButtonStyle.secondary,
                    label="Maybe",
                    custom_id="rsvp:maybe",
                ),
            ),
            accent_colour=0x57f287,
        ),
    )
    return view


# Buttons and menus with a custom_id do nothing until your bot handles them:
# subclass ui.LayoutView with callbacks, or listen for on_interaction.

# Send it from any coroutine: a command, an event listener, a task.
await channel.send(view=build_view())

Files, mentions and silent messages

  • An uploaded file is not shown on its own. Reference it from a Thumbnail, MediaGallery or File item as attachment://name and pass discord.File objects with that same filename in files=.
  • allowed_mentions= works as it does everywhere else in discord.py. Pass discord.AllowedMentions.none() for a layout whose text mentions roles you do not want pinged.
  • silent=True sends without a push notification, exactly as it does for an ordinary message.
  • message.edit(view=new_view) replaces the layout. A message sent as Components V2 stays one: an edit cannot return it to content and embeds.

Sending through a webhook

discord.Webhook.send() and discord.SyncWebhook.send() both take view=. When you pass one, discord.py adds the with_components query parameter to the request for you — the detail that makes hand-written webhook requests fail silently, because without it Discord accepts the call and discards the components.

A webhook your application does not own can carry layout items and link buttons only. Items with a custom_id need an application-owned webhook, since only an application can receive the interaction. If you are not running a bot at all, post to the webhook URL with requests instead.

Design it visually, then export the code

Nested ui calls are slow to iterate on: you cannot see the layout until the bot sends it. DWEEB's code generator works the other way round — build the message in the visual editor against a live preview, open the Code tab, and copy it as discord.py, with the imports, the view and the send call included. Both samples on this page were produced that way.

Any of the ready-made templates can be the starting point, and each template page shows its own discord.py and discord.js code. If Discord rejects a payload, the error guide explains how to read the field path in its response.

Put the guide into practice

Open the exact workflow in DWEEB. Nothing posts until you review and confirm it.

Design a message and export discord.py code →

Primary sources

Keep learning