LogoRobo.js

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.

src/commands/info.ts
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] }
}
src/commands/info.js
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

MethodDescription
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.

ElementLimit
Title256 characters
Description4096 characters
Fields25 per embed
Field name256 characters
Field value1024 characters
Footer text2048 characters
Author name256 characters
Total characters6000 across all elements
Embeds per message10
src/commands/profile.ts
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] }
}
src/commands/profile.js
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:

src/commands/dashboard.ts
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] }
}
src/commands/dashboard.js
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:

src/commands/leaderboard.ts
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] }
}
src/commands/leaderboard.js
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.

src/commands/fetch-data.ts
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] }
}
src/commands/fetch-data.js
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:

src/commands/poll.ts
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] }
}
src/commands/poll.js
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.

Next steps

On this page