Skip to main content

Render Modes

Every FlowCord menu renders in one of two modes: embeds mode or layout mode. The choice is made per menu and is mutually exclusive — you can't mix the two within a single menu definition.

Embeds mode

The default and most broadly compatible mode. Uses Discord's traditional embed + action row model.

new MenuBuilder(session, 'profile')
.setEmbeds((ctx) => [
new EmbedBuilder()
.setTitle(ctx.state.get('username'))
.setDescription('Level 42 Trainer')
.setColor(0x5865f2),
])
.setButtons((ctx) => [
{ label: 'Edit Bio', style: ButtonStyle.Primary, action: async (ctx) => { /* ... */ } },
{ label: 'View Badges', style: ButtonStyle.Secondary, action: goTo('badges') },
])
.build()

Builder methods available in embeds mode:

  • setEmbeds(fn) — one or more EmbedBuilder instances per render
  • setButtons(fn, options?) — buttons with optional pagination
  • setSelectMenu(fn) — a single select menu (any Discord.js select type)

Layout mode

Uses Discord Components v2, which allows richer, more structured layouts using display components like text blocks, sections, containers, and media galleries. Discord sends this with the IsComponentsV2 message flag.

import { text, section, container, button, separator } from '@flowcord/core';

new MenuBuilder(session, 'item-shop')
.setLayout((ctx) => [
container({
accentColor: 0x5865f2,
children: [
text('## Item Shop'),
separator({ divider: true }),
section({
text: [text('**Iron Sword** — 150 coins')],
accessory: button({
label: 'Buy',
style: ButtonStyle.Success,
action: async (ctx) => { /* ... */ },
}),
}),
],
}),
])
.build()

Builder methods available in layout mode:

  • setLayout(fn) — returns an array of component configs built with the display component helpers
  • Interactive components (button(), select(), actionRow(), paginatedGroup()) are composed directly into the layout tree

See Layout Mode for the full display component reference.

Mutual exclusivity

Calling both setEmbeds() and setLayout() on the same builder is a compile-time error — TypeScript narrows the builder's TMode generic when you call either method, making the other unavailable.

new MenuBuilder(session, 'example')
.setEmbeds(() => [...]) // TMode narrows to 'embeds'
.setLayout(() => [...]) // ❌ TypeScript error — setLayout not available in embeds mode

If you somehow bypass the type system, .build() will throw at runtime as a safety net.

Choosing a mode

Embeds modeLayout mode
Discord client supportAll clientsNewer clients (Components v2)
EmbedsEmbedBuilderNot available
ButtonssetButtons() with paginationbutton() composed into layout
Select menussetSelectMenu()select() composed into layout
Display componentsNot availabletext(), section(), container(), separator(), thumbnail(), mediaGallery(), file()
Button paginationsetButtons(..., { pagination })paginatedGroup()
List paginationsetListPagination()setListPagination()
ModalssetModal()setModal()

Reach for embeds mode when:

  • You want broad Discord client compatibility
  • Your UI is primarily information-dense embeds
  • You need the button pagination system

Reach for layout mode when:

  • You want rich structured layouts (sections with accessories, nested containers, media)
  • You're comfortable with Components v2's client requirements
  • Your UI is more compositional than embed-centric

Both modes support lifecycle hooks, navigation, state, modals, and list pagination identically.