djs-commandsv2 docs
Components V2

Components V2

JSX runtime and a function-call fallback for Discord's new component model.

Discord's Components V2 message format replaces the old "embed + button rows" pattern with composable layout primitives — Containers, Sections, Text Displays, Media Galleries, Separators, Files, and Action Rows of Buttons.

djs-commands ships two ways to build them. Pick whichever fits your project:

  • @djs-commands/jsx — write components as JSX, like React. Best DX if your toolchain (TS, Bun, ESBuild, SWC) already supports JSX.
  • Function fallback from @djs-commands/core — the same builders, exposed as plain functions. No JSX pragma, no extra dependency.

Both produce real discord.js builder objects, so you can mix the two freely.

Quick start (JSX)

bun add @djs-commands/jsx

Configure TypeScript to use the runtime:

tsconfig.json
{
    "compilerOptions": {
        "jsx": "react-jsx",
        "jsxImportSource": "@djs-commands/jsx"
    }
}

That's it — no Babel, no SWC. Bun's transpiler and TypeScript both honor jsxImportSource directly.

src/welcome.tsx
import { defineCommand } from "@djs-commands/core";
import { Button, Container, Section, TextDisplay, render } from "@djs-commands/jsx";
import { MessageFlags } from "discord.js";

export const welcome = defineCommand({
    name: "welcome",
    description: "Show a Components V2 welcome card",
    run: async ({ interaction }) => {
        await interaction.reply({
            flags: MessageFlags.IsComponentsV2,
            components: render(
                <Container accentColor={0x5865f2}>
                    <TextDisplay># Welcome</TextDisplay>
                    <Section accessory={<Button style="primary" customId="welcome:next" label="Next" />}>
                        Components V2 lets you compose rich messages from layout primitives.
                    </Section>
                </Container>
            ),
        });
    },
});

Quick start (function fallback)

If you can't (or won't) enable JSX, every component has a function-form sibling re-exported from @djs-commands/core. They return the same discord.js builders.

src/welcome.ts
import { button, container, defineCommand, section, textDisplay } from "@djs-commands/core";
import { MessageFlags } from "discord.js";

export const welcome = defineCommand({
    name: "welcome",
    description: "Show a Components V2 welcome card",
    run: async ({ interaction }) => {
        await interaction.reply({
            flags: MessageFlags.IsComponentsV2,
            components: [
                container({
                    accentColor: 0x5865f2,
                    children: [
                        textDisplay("# Welcome"),
                        section({
                            accessory: button({ style: "primary", customId: "welcome:next", label: "Next" }),
                            text: "Components V2 lets you compose rich messages from layout primitives.",
                        }),
                    ],
                }),
            ],
        });
    },
});

Side-by-side: same output

The two APIs are isomorphic. Here's the identical message expressed both ways:

render(
    <Container accentColor={0xff6b35}>
        <TextDisplay># Bug report</TextDisplay>
        <Section accessory={<Button style="link" url="https://github.com/owner/repo" label="Repo" />}>
            File an issue if something looks off.
        </Section>
        <Separator divider={true} />
        <ActionRow>
            <Button style="primary" customId="bug:reopen" label="Reopen" />
            <Button style="danger" customId="bug:close" label="Close" />
        </ActionRow>
    </Container>
);
container({
    accentColor: 0xff6b35,
    children: [
        textDisplay("# Bug report"),
        section({
            accessory: button({ style: "link", url: "https://github.com/owner/repo", label: "Repo" }),
            text: "File an issue if something looks off.",
        }),
        separator({ divider: true }),
        actionRow({
            children: [button({ style: "primary", customId: "bug:reopen", label: "Reopen" }), button({ style: "danger", customId: "bug:close", label: "Close" })],
        }),
    ],
});

Modals

Both runtimes can build modals. The JSX form mirrors the message API:

import { Modal, TextInput, renderModal } from "@djs-commands/jsx";

await interaction.showModal(
    renderModal(
        <Modal title="Send Feedback" customId="feedback-modal">
            <TextInput customId="subject" label="Subject" style="short" required={true} />
            <TextInput customId="body" label="What's on your mind?" style="paragraph" />
        </Modal>
    )
);
import { modal, textInput } from "@djs-commands/core";

await interaction.showModal(
    modal({
        title: "Send Feedback",
        customId: "feedback-modal",
        fields: [textInput({ customId: "subject", label: "Subject", style: "short", required: true }), textInput({ customId: "body", label: "What's on your mind?", style: "paragraph" })],
    })
);

<RadioGroup> and <CheckboxGroup> are also available for modal forms; the function-form equivalents are radioGroup() and checkboxGroup().

What you can build

The full component set covers everything Discord exposes today:

LayoutForm
ContainerActionRow
SectionButton
TextDisplayModal
MediaGalleryTextInput
SeparatorRadioGroup
FileCheckboxGroup
Thumbnail

Full example

For a runnable bot covering every primitive — slash command, container, section, gallery, separator, action row, and a modal — see examples/components-v2-showcase.

Interaction routing (a click in <Button customId="..." /> reaching a typed handler) is the next slice's focus. Today, you wire button/modal interactions on client.on(Events.InteractionCreate, ...) as in the showcase example.

On this page