Embeds
Rich embedded messages with fields, images, and dynamic content.
Embeds are rich content blocks that Discord renders with special formatting -- colored borders, titles, fields, images, and more. They are built using Discord.js's EmbedBuilder and returned from command handlers.
Creating embeds
Build an embed with EmbedBuilder and return it in an embeds array. Sage mode wraps the return value as a reply automatically.
import { EmbedBuilder } from 'discord.js'
import type { ChatInputCommandInteraction } from 'discord.js'
export default (interaction: ChatInputCommandInteraction) => {
const embed = new EmbedBuilder()
.setTitle('Server Info')
.setDescription('Welcome to our server!')
.setColor(0x5865f2)
return { embeds: [embed] }
}import { EmbedBuilder } from 'discord.js'
export default (interaction) => {
const embed = new EmbedBuilder()
.setTitle('Server Info')
.setDescription('Welcome to our server!')
.setColor(0x5865f2)
return { embeds: [embed] }
}EmbedBuilder methods
| Method | Description |
|---|---|
setTitle(title) | Set the embed title (max 256 characters) |
setDescription(text) | Set the embed body text (max 4096 characters) |
setColor(color) | Set the left border color (hex number or color constant) |
setURL(url) | Make the title a clickable link |
setThumbnail(url) | Small image in the top-right corner |
setImage(url) | Large image below the description |
setAuthor({ name, iconURL?, url? }) | Author section above the title (name max 256 characters) |
setFooter({ text, iconURL? }) | Footer text at the bottom (max 2048 characters) |
setTimestamp(date?) | Timestamp next to the footer (defaults to now) |
addFields(...fields) | Add one or more fields (see below) |
spliceFields(index, count, ...fields) | Remove/replace fields at a position |
toJSON() | Serialize to a plain object |
Embed limits
Discord enforces strict limits on embed content. Exceeding any limit causes the API call to fail.
| Element | Limit |
|---|---|
| Title | 256 characters |
| Description | 4096 characters |
| Fields | 25 per embed |
| Field name | 256 characters |
| Field value | 1024 characters |
| Footer text | 2048 characters |
| Author name | 256 characters |
| Total characters | 6000 across all elements |
| Embeds per message | 10 |
Full-featured embed
import { EmbedBuilder } from 'discord.js'
import type { CommandConfig } from '@robojs/discordjs'
import type { ChatInputCommandInteraction, User } from 'discord.js'
export const config: CommandConfig = {
description: 'View a user profile',
options: [{ name: 'user', type: 'user', required: true }]
}
export default (interaction: ChatInputCommandInteraction, options: { user: User }) => {
const embed = new EmbedBuilder()
.setTitle(`${options.user.username}'s Profile`)
.setDescription('Member details and stats')
.setColor(0x57f287)
.setThumbnail(options.user.displayAvatarURL())
.addFields(
{ name: 'Joined Discord', value: options.user.createdAt.toDateString(), inline: true },
{ name: 'User ID', value: options.user.id, inline: true },
{ name: 'Bot', value: options.user.bot ? 'Yes' : 'No', inline: true }
)
.setFooter({ text: `Requested by ${interaction.user.username}` })
.setTimestamp()
return { embeds: [embed] }
}import { EmbedBuilder } from 'discord.js'
export const config = {
description: 'View a user profile',
options: [{ name: 'user', type: 'user', required: true }]
}
export default (interaction, options) => {
const embed = new EmbedBuilder()
.setTitle(`${options.user.username}'s Profile`)
.setDescription('Member details and stats')
.setColor(0x57f287)
.setThumbnail(options.user.displayAvatarURL())
.addFields(
{ name: 'Joined Discord', value: options.user.createdAt.toDateString(), inline: true },
{ name: 'User ID', value: options.user.id, inline: true },
{ name: 'Bot', value: options.user.bot ? 'Yes' : 'No', inline: true }
)
.setFooter({ text: `Requested by ${interaction.user.username}` })
.setTimestamp()
return { embeds: [embed] }
}Multiple embeds
Send up to 10 embeds in a single message by adding them to the embeds array:
import { EmbedBuilder } from 'discord.js'
export default () => {
const statsEmbed = new EmbedBuilder()
.setTitle('Server Stats')
.setColor(0x5865f2)
.addFields({ name: 'Members', value: '1,234', inline: true })
const rulesEmbed = new EmbedBuilder()
.setTitle('Server Rules')
.setColor(0xed4245)
.setDescription('1. Be respectful\n2. No spam\n3. Have fun!')
return { embeds: [statsEmbed, rulesEmbed] }
}import { EmbedBuilder } from 'discord.js'
export default () => {
const statsEmbed = new EmbedBuilder()
.setTitle('Server Stats')
.setColor(0x5865f2)
.addFields({ name: 'Members', value: '1,234', inline: true })
const rulesEmbed = new EmbedBuilder()
.setTitle('Server Rules')
.setColor(0xed4245)
.setDescription('1. Be respectful\n2. No spam\n3. Have fun!')
return { embeds: [statsEmbed, rulesEmbed] }
}Inline fields
Fields with inline: true display side by side in Discord's 3-column grid layout. Discord wraps to the next row after every 3 inline fields.
embed.addFields(
{ name: 'HP', value: '100/100', inline: true },
{ name: 'MP', value: '50/50', inline: true },
{ name: 'Level', value: '42', inline: true },
{ name: 'Bio', value: 'A long description that spans the full width.' }
)A non-inline field always starts a new row.
Dynamic embeds
Build embeds from dynamic data by mapping arrays to fields:
import { EmbedBuilder } from 'discord.js'
const scores = [
{ name: 'Alice', score: 9500 },
{ name: 'Bob', score: 8200 },
{ name: 'Charlie', score: 7100 }
]
export default () => {
const medals = ['🥇', '🥈', '🥉']
const embed = new EmbedBuilder()
.setTitle('Leaderboard')
.setColor(0xfee75c)
.addFields(
scores.map((entry, i) => ({
name: `${medals[i] ?? ''} #${i + 1} ${entry.name}`,
value: `${entry.score.toLocaleString()} points`,
inline: false
}))
)
return { embeds: [embed] }
}import { EmbedBuilder } from 'discord.js'
const scores = [
{ name: 'Alice', score: 9500 },
{ name: 'Bob', score: 8200 },
{ name: 'Charlie', score: 7100 }
]
export default () => {
const medals = ['🥇', '🥈', '🥉']
const embed = new EmbedBuilder()
.setTitle('Leaderboard')
.setColor(0xfee75c)
.addFields(
scores.map((entry, i) => ({
name: `${medals[i] ?? ''} #${i + 1} ${entry.name}`,
value: `${entry.score.toLocaleString()} points`,
inline: false
}))
)
return { embeds: [embed] }
}Editing embeds
Use interaction.editReply() to update an embed after async work. This works with Sage's auto-defer -- when your handler takes longer than the defer buffer, Sage defers automatically, and you can edit the reply with updated content.
import { EmbedBuilder } from 'discord.js'
import type { ChatInputCommandInteraction } from 'discord.js'
export default async (interaction: ChatInputCommandInteraction) => {
// Sage auto-defers if this takes > 250ms
const data = await fetchExternalApi()
const embed = new EmbedBuilder()
.setTitle('API Results')
.setDescription(data.summary)
.setColor(0x57f287)
return { embeds: [embed] }
}import { EmbedBuilder } from 'discord.js'
export default async (interaction) => {
const data = await fetchExternalApi()
const embed = new EmbedBuilder()
.setTitle('API Results')
.setDescription(data.summary)
.setColor(0x57f287)
return { embeds: [embed] }
}Embeds with components
Combine embeds with action rows (buttons, select menus) in a single reply:
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'
export default () => {
const embed = new EmbedBuilder()
.setTitle('Quick Poll')
.setDescription('Do you like embeds?')
.setColor(0x5865f2)
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId('poll-yes').setLabel('Yes').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId('poll-no').setLabel('No').setStyle(ButtonStyle.Danger)
)
return { embeds: [embed], components: [row] }
}import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'
export default () => {
const embed = new EmbedBuilder()
.setTitle('Quick Poll')
.setDescription('Do you like embeds?')
.setColor(0x5865f2)
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId('poll-yes').setLabel('Yes').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId('poll-no').setLabel('No').setStyle(ButtonStyle.Danger)
)
return { embeds: [embed], components: [row] }
}See Components for the full reference on buttons and select menus.
