Buttons are interactive components that users can click to trigger actions in your Discord bot.
src/components/buttons/. The customId is generated from the file path, so you don't need to set it manually.Buttons in djs-core are created using the Button class. Each button component file in src/components/buttons/ is automatically registered.
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("Click me!")
.setStyle(ButtonStyle.Primary)
.setCustomId("my-button")
.run(async (interaction) => {
await interaction.reply("Button clicked!");
});
Discord provides several button styles to visually communicate different actions:
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("Confirm")
.setStyle(ButtonStyle.Primary)
.setCustomId("confirm-button")
.run(async (interaction) => {
await interaction.reply("Confirmed!");
});
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("Delete")
.setStyle(ButtonStyle.Danger)
.setCustomId("delete-button")
.run(async (interaction) => {
await interaction.reply("Item deleted!");
});
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("Save")
.setStyle(ButtonStyle.Success)
.setCustomId("save-button")
.run(async (interaction) => {
await interaction.reply("Saved successfully!");
});
Buttons are typically sent as part of a command or context menu response:
import { Command, Button } from "@djs-core/runtime";
import { ButtonStyle, ActionRowBuilder } from "discord.js";
import myButton from "../../components/buttons/my-button";
export default new Command()
.setDescription("Show a button")
.run(async (interaction) => {
const row = new ActionRowBuilder<Button>().addComponents(myButton);
await interaction.reply({
content: "Click the button below!",
components: [row],
});
});
Buttons can receive custom data that is passed to the handler when clicked. Important: The data is not set in the component definition, but rather when you use the component from a command or another component.
In your component file, define the button with the expected data type, but don't set the data:
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button<{ userId: string; action: string }>()
.setLabel("Confirm")
.setStyle(ButtonStyle.Success)
.setCustomId("confirm-button")
.run(async (interaction, data) => {
await interaction.reply(
`Processing ${data.action} for user ${data.userId}`,
);
});
When using the button from a command, set the data directly on the imported component:
import { Command, Button } from "@djs-core/runtime";
import { ActionRowBuilder } from "discord.js";
import confirmButton from "../../components/buttons/confirm";
export default new Command()
.setDescription("Delete an item")
.run(async (interaction) => {
const userId = interaction.user.id;
// Set data when using the button
const row = new ActionRowBuilder<Button>().addComponents(
confirmButton.setData({ userId, action: "delete" }),
);
await interaction.reply({
content: "Are you sure you want to delete?",
components: [row],
});
});
You can pass dynamic data based on the command context:
import { Command, Button } from "@djs-core/runtime";
import { ActionRowBuilder } from "discord.js";
import confirmButton from "../../components/buttons/confirm";
export default new Command()
.setDescription("Delete item")
.addStringOption((option) =>
option
.setName("item-id")
.setDescription("The item ID to delete")
.setRequired(true),
)
.run(async (interaction) => {
const itemId = interaction.options.getString("item-id");
const userId = interaction.user.id;
// Set data dynamically based on command input
const row = new ActionRowBuilder<Button>().addComponents(
confirmButton.setData({ userId, action: `delete_item_${itemId}` }),
);
await interaction.reply({
content: `Are you sure you want to delete item ${itemId}?`,
components: [row],
});
});
You can optionally specify a time-to-live (in seconds) for the data:
import { Command, Button } from "@djs-core/runtime";
import { ActionRowBuilder } from "discord.js";
import confirmButton from "../../components/buttons/confirm";
export default new Command()
.setDescription("Time-limited action")
.run(async (interaction) => {
// Data expires after 60 seconds
const row = new ActionRowBuilder<Button>().addComponents(
confirmButton.setData({ userId: interaction.user.id }, 60),
);
await interaction.reply({
content: "This button will expire in 60 seconds",
components: [row],
});
});
You can add emojis to buttons:
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("React")
.setStyle(ButtonStyle.Primary)
.setCustomId("react-button")
.setEmoji("👍")
.run(async (interaction) => {
await interaction.reply("Thanks!");
});
Buttons can be disabled:
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("Disabled")
.setStyle(ButtonStyle.Secondary)
.setCustomId("disabled-button")
.setDisabled(true)
.run(async (interaction) => {
await interaction.reply("This won't be called when disabled");
});
Button responses can be ephemeral (only visible to the user who clicked):
import { Button } from "@djs-core/runtime";
import { ButtonStyle, MessageFlags } from "discord.js";
export default new Button()
.setLabel("Private Action")
.setStyle(ButtonStyle.Primary)
.setCustomId("private-button")
.run(async (interaction) => {
await interaction.reply({
content: "This is only visible to you!",
flags: [MessageFlags.Ephemeral],
});
});
Buttons can be organized in subdirectories:
src/components/buttons/
├── confirm.ts
├── cancel.ts
└── admin/
├── delete.ts
└── ban.ts
Link buttons navigate to external URLs and don't trigger interaction events. Since they don't have a .run() handler, they're often simpler to create.
If a link button is only used in one place, you can create it directly in your command without creating a separate component file:
import { Command, Button } from "@djs-core/runtime";
import { ButtonStyle, ActionRowBuilder } from "discord.js";
export default new Command()
.setDescription("Visit our website")
.run(async (interaction) => {
// Create the link button directly in the command
const linkButton = new Button()
.setLabel("Visit Website")
.setStyle(ButtonStyle.Link)
.setURL("https://example.com");
const row = new ActionRowBuilder<Button>().addComponents(linkButton);
await interaction.reply({
content: "Check out our website!",
components: [row],
});
});
If you need to reuse a link button across multiple commands, create it as a separate component file:
import { Button } from "@djs-core/runtime";
import { ButtonStyle } from "discord.js";
export default new Button()
.setLabel("Visit Website")
.setStyle(ButtonStyle.Link)
.setURL("https://example.com");
// Note: Link buttons don't have a .run() handler
Then import and use it in your commands:
import { Command, Button } from "@djs-core/runtime";
import { ActionRowBuilder } from "discord.js";
import websiteLink from "../../components/buttons/website-link";
export default new Command()
.setDescription("Get help")
.run(async (interaction) => {
const row = new ActionRowBuilder<Button>().addComponents(websiteLink);
await interaction.reply({
content: "Visit our website for more information!",
components: [row],
});
});