DWEEB
Developer guide · discord.js

Components V2 in discord.js

Components V2 replaces content and embeds with a tree of layout components, and discord.js has a builder class for each one. This guide maps every component to its builder, shows complete working messages, and covers the rules that change once the V2 flag is set.

What discord.js needs for Components V2

Components V2 support arrived in discord.js 14.19.0. From that release the library ships a builder for every layout component and MessageFlags gains IsComponentsV2. How you send does not change: channel.send(), interaction.reply() and message.edit() take the new builders in the same components array that used to hold only action rows.

The flag is what switches a message to the new layout system, and it changes the rules for that message. With it set, Discord rejects content, embeds, poll and stickers, so every piece of visible text has to live in a Text Display component. A message may hold 40 components in total, nested ones included, and 4,000 characters of text across all of them.

Every component and its builder

A container takes its children through one typed method per kind — addTextDisplayComponents, addSectionComponents, addMediaGalleryComponents, addSeparatorComponents, addFileComponents and addActionRowComponents — and renders them in the order the calls are made. That is the detail most hand-written examples get wrong: two text blocks separated by a gallery need three calls, not two.

Every component and its builder
Component (type)discord.js builderWhere it goes
Container (17)ContainerBuilderTop level only. Holds every kind below except another container
Text Display (10)TextDisplayBuilderTop level, inside a container, or inside a section
Section (9)SectionBuilderOne to three text displays plus exactly one accessory
Thumbnail (11)ThumbnailBuilderA section's accessory
Media Gallery (12)MediaGalleryBuilder with MediaGalleryItemBuilderOne to 10 images or videos
Separator (14)SeparatorBuilderA divider line or plain spacing, sized with SeparatorSpacingSize
File (13)FileBuilderAn uploaded file, referenced as attachment://name
Action Row (1)ActionRowBuilderUp to 5 buttons, or one select menu
Button (2)ButtonBuilderIn an action row, or as a section's accessory
Select menus (3, 5–8)StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, MentionableSelectMenuBuilder, ChannelSelectMenuBuilderOne per action row

A complete container message

This is the embed-style card most bots start with: an accent-striped container holding a heading, a divider and a link button. It is the same message as the minimal JSON payload in the Components V2 reference, so you can compare the builder calls with the wire format line by line.

// Components V2 message — discord.js v14.19 or newer.
// Designed in DWEEB (https://dweeb.faizo.net). Edit the text, then send.
const {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  ContainerBuilder,
  MessageFlags,
  SeparatorBuilder,
  SeparatorSpacingSize,
  TextDisplayBuilder,
} = require("discord.js");

const container = new ContainerBuilder()
  .setAccentColor(0x5865f2)
  .addTextDisplayComponents(
    new TextDisplayBuilder().setContent(
      [
        "# Server update",
        "Everything you need in one place.",
      ].join("\n"),
    ),
  )
  .addSeparatorComponents(
    new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small),
  )
  .addActionRowComponents(
    new ActionRowBuilder().addComponents(
      new ButtonBuilder()
        .setStyle(ButtonStyle.Link)
        .setLabel("Read the guide")
        .setURL("https://example.com/update"),
    ),
  );

// Send it from any async context: a command handler, an event, a script.
await channel.send({
  components: [container],
  flags: MessageFlags.IsComponentsV2,
});

A section with a thumbnail, and buttons that do something

A Section puts text beside one accessory — a thumbnail here, or a single button. It is the layout a legacy embed could not express, and the reason many bots move to Components V2 at all.

The two buttons below carry a custom ID, so clicking one sends your bot an interaction and nothing else happens until you answer it. Listen for interactionCreate, check interaction.isButton(), and branch on interaction.customId. A link button needs none of that: Discord opens its URL itself.

// Components V2 message — discord.js v14.19 or newer.
// Designed in DWEEB (https://dweeb.faizo.net). Edit the text, then send.
const {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  ContainerBuilder,
  MessageFlags,
  SectionBuilder,
  TextDisplayBuilder,
  ThumbnailBuilder,
} = require("discord.js");

const container = new ContainerBuilder()
  .setAccentColor(0x57f287)
  .addSectionComponents(
    new SectionBuilder()
      .addTextDisplayComponents(
        new TextDisplayBuilder().setContent("## Friday game night"),
        new TextDisplayBuilder().setContent(
          "Doors open at 8 PM. Bring a friend and a headset.",
        ),
      )
      .setThumbnailAccessory(
        new ThumbnailBuilder()
          .setURL("https://example.com/game-night.png")
          .setDescription("Game night artwork"),
      ),
  )
  .addActionRowComponents(
    new ActionRowBuilder().addComponents(
      new ButtonBuilder()
        .setStyle(ButtonStyle.Success)
        .setLabel("I'm in")
        .setCustomId("rsvp:yes"),
      new ButtonBuilder()
        .setStyle(ButtonStyle.Secondary)
        .setLabel("Maybe")
        .setCustomId("rsvp:maybe"),
    ),
  );

// Buttons and menus with a custom ID do nothing until your bot handles the
// interaction: client.on("interactionCreate", …) and match on customId.

// Send it from any async context: a command handler, an event, a script.
await channel.send({
  components: [container],
  flags: MessageFlags.IsComponentsV2,
});

Replies, ephemeral messages and edits

  • interaction.reply() and interaction.followUp() take the same components array. Pass flags: MessageFlags.IsComponentsV2 there as well.
  • For an ephemeral V2 reply, combine the flags with a bitwise OR: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral.
  • message.edit({ components }) replaces the whole layout. The V2 flag stays on a message for good, so an edit cannot take it back to content and embeds.
  • Uploaded files are not displayed on their own. Every attachment has to be referenced from a Thumbnail, Media Gallery or File component as attachment://filename, with the file passed in files under that same name.

Sending through a webhook instead of a bot

WebhookClient#send accepts the same builders and the same flag. A webhook that your application does not own also needs withComponents: true, which discord.js turns into the with_components=true query parameter. Leave it out and Discord accepts the request but drops the components, which looks exactly like a message that was sent empty.

Such a webhook can carry layout components and link buttons. Buttons and menus with a custom ID need an application-owned webhook, because only an application can receive the interaction they produce. If you are not running a bot at all, post to the webhook URL with fetch instead.

Errors you are likely to meet

The builders validate eagerly, so a bad URL or an empty label throws in your own process with the field name, before any request is made. When Discord itself refuses a payload, the webhook and API error guide explains how to read the field path in its response.

  • The flag is missing: Discord does not recognise a container or a text display on a message that was not marked as V2, and rejects the payload.
  • content or embeds are still in the call: they are refused as soon as the flag is set. Move the text into a TextDisplayBuilder.
  • A section has no accessory: one thumbnail or one button is required. If you only want text, use text displays without a section.
  • More than 40 components: nested children count, so a container with several sections reaches the ceiling sooner than the visible layout suggests.
  • An attachment:// URL that matches no uploaded file name: the message is rejected rather than shown with a broken image.

Design it visually, then export the code

Writing a layout as nested builder calls is slow to iterate on: you cannot see the result 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, then open the Code tab and copy it as discord.js — imports, builders, flag and send call included. Every sample on this page was produced that way.

Any of the ready-made templates can be the starting point, and each template page shows its own discord.js and discord.py code.

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.js code →

Primary sources

Keep learning