# Buttons & Select Menus

URL: https://zerodev.ca/docs/medusa/developer-documentation/components

> Exhaustive reference for Buttons, Select Menus, Components V2, and Medusa's internal Discord UI API.



Medusa provides a unified internal API for building, resolving, tokenizing, and dispatching Discord message components. Components are built with the `this.medusa.components` builder and resolved and dispatched through the Embeds service (`this.medusa.embeds`). The builder produces plain component trees with named methods so module code never hardcodes Discord's raw component type integers.

<Callout type="warn">
  Never use numeric integers for button styles (such as `style: 2` or `style: 1`). Medusa requires string literals (`"primary"`, `"secondary"`, `"success"`, `"danger"`, `"link"`). Medusa's internal component resolver automatically normalizes string literals to Discord's underlying numeric codes during dispatch.
</Callout>

<Callout>
  Official modules use gray `"secondary"` buttons (or `"link"` for URLs) everywhere. Only user-configured button colour options pick other styles.
</Callout>

***

## Component Architecture

Discord components in Medusa operate in two modes:

1. **Standard Components (V1)**: Up to 5 Action Rows (`type: 1`), each holding up to 5 buttons (`type: 2`) or 1 select menu (`type: 3`).
2. **Components V2 (`MessageFlags.IsComponentsV2`)**: Modern Discord card layouts utilizing Containers (`type: 17`), Sections (`type: 9`), Text Displays (`type: 10`), Separators (`type: 14`), Thumbnails (`type: 11`), and Media Galleries (`type: 12`), in addition to Action Rows and Section button accessories.

Build trees with `this.medusa.components` (see [Builder API](#builder-api-thismedusacomponents)), then let the Embeds service resolve them. Every tree passes through `this.medusa.embeds.resolveComponent(comp, tokens)` (or `this.medusa.components.resolve` / `payload` / `send`, which call it) to ensure token replacement, custom emoji translation, style conversion, and accent color injection. `this.medusa.embeds.buildComponents(buttons, selectMenu, rawComponents)` remains available for assembling standard button rows.

***

## Builder API (`this.medusa.components`)

Every building block (handlers, commands, events, routes) receives `this.medusa.components`, a builder that returns plain component trees. Each method maps to a Discord component type so module code never writes raw integers like `type: 17` or `type: 10`. The trees flow unchanged into `resolve`, `payload`, `send`, or `this.medusa.embeds.send(...)`, where tokens, styles, emojis, and accent colors are resolved.

### Builder Reference

| Method      | Signature                                                                   | Produces                                                                                                                      |
| :---------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- |
| `container` | `({ accent?, components?, spoiler?, id? }) => object`                       | Container (`type: 17`). `accent` is accepted for design purposes, but the resolver always applies the global `ACCENT_COLOUR`. |
| `section`   | `({ components?, accessory? }) => object`                                   | Section (`type: 9`). String `components` are wrapped as text displays; `accessory` is a `button` or `thumbnail`.              |
| `text`      | `(content: string) => object`                                               | Text Display (`type: 10`).                                                                                                    |
| `separator` | `({ divider?, spacing? }) => object`                                        | Separator (`type: 14`). Defaults to `divider: true`, `spacing: 1`.                                                            |
| `thumbnail` | `(url: string, { description?, spoiler? }) => object`                       | Thumbnail (`type: 11`), used as a section accessory.                                                                          |
| `gallery`   | `(items: Array<string \| object>) => object`                                | Media Gallery (`type: 12`). Items are URL strings or `{ url, description?, spoiler? }`.                                       |
| `file`      | `(url: string, { spoiler? }) => object`                                     | File (`type: 13`).                                                                                                            |
| `row`       | `(components: Array) => object`                                             | Action Row (`type: 1`) holding buttons or a select menu.                                                                      |
| `button`    | `({ style?, label?, custom_id?, url?, emoji?, disabled? }) => object`       | Button (`type: 2`) with a string `style`.                                                                                     |
| `select`    | `({ custom_id?, placeholder?, options?, disabled?, min?, max? }) => object` | String Select Menu (`type: 3`).                                                                                               |
| `resolve`   | `(components, tokens?) => Array`                                            | Resolves one tree or an array of trees through the Embeds resolver, returning the dispatch-ready array.                       |
| `payload`   | `(components, { tokens?, content?, silent? }) => object`                    | Resolves and wraps a tree into a send-ready message object with `MessageFlags.IsComponentsV2` applied.                        |
| `send`      | `(channelId, components, options?) => Promise<Message>`                     | Builds a `payload` and posts it to a channel.                                                                                 |

### Building a V2 Card

```javascript
const banner = this.medusa.components.container({
    accent: "#ef4444",
    components: [
        this.medusa.components.section({
            components: [`### ${this.medusa.modules.lang.phrase("anti-scam", "messages", "bannerTitle")}\n\n${this.medusa.modules.lang.phrase("anti-scam", "messages", "bannerBody")}`],
            accessory: this.medusa.components.thumbnail("%brand_icon%")
        }),
        this.medusa.components.separator(),
        this.medusa.components.row([
            this.medusa.components.button({
                style: "success",
                label: this.medusa.modules.lang.phrase("example", "messages", "confirm"),
                emoji: "%custom_emoji_check%",
                custom_id: "medusa:button:anti-scam:confirm"
            })
        ])
    ]
});

await channel.send(this.medusa.components.payload(banner));
```

`payload` applies `MessageFlags.IsComponentsV2` and resolves the whole tree, so handlers no longer set the flag or call `resolveComponent` by hand. Pass `{ silent: true }` to suppress notifications, `{ content }` for a leading message, and `{ tokens }` for custom `%token%` substitutions.

***

## Buttons (`type: 2`)

Buttons allow users to trigger bot workflows, confirm actions, or open external links.

### Button Schema

```typescript
type ButtonStyle = "primary" | "secondary" | "success" | "danger" | "link";

interface ButtonComponent {
    type: 2;
    style: ButtonStyle;
    label?: string;
    custom_id?: string;
    url?: string;
    emoji?: string | { id?: string; name: string; animated?: boolean };
    disabled?: boolean;
}
```

| Field       | Type               | Description                                                                                                      |
| :---------- | :----------------- | :--------------------------------------------------------------------------------------------------------------- |
| `type`      | `2`                | Discord component type for Buttons.                                                                              |
| `style`     | `string`           | **Required.** String literal style: `"primary"`, `"secondary"`, `"success"`, `"danger"`, or `"link"`.            |
| `label`     | `string`           | Display text on the button (max 80 characters). Automatically trimmed. Defaults to URL or `Button N` if omitted. |
| `custom_id` | `string`           | Interaction identifier returned when the button is clicked. Required unless `style: "link"`.                     |
| `url`       | `string`           | Target HTTP/HTTPS destination URL. Required when `style: "link"`. Must match `/^https?:\/\//i`.                  |
| `emoji`     | `string \| object` | Custom bot emoji token (e.g. `%custom_emoji_hammer%`), unicode emoji, or `{ id, name, animated }`.               |
| `disabled`  | `boolean`          | When `true`, disables user interaction and renders the button visually dimmed.                                   |

### Button Styles

| Style Literal | Underlying Discord Code | Appearance              | Usage & Requirements                                                      |
| :------------ | :---------------------- | :---------------------- | :------------------------------------------------------------------------ |
| `"primary"`   | `1`                     | Blurple / Accent        | Primary call-to-action button. Requires `custom_id`.                      |
| `"secondary"` | `2`                     | Neutral Grey            | Default secondary button or neutral navigation. Requires `custom_id`.     |
| `"success"`   | `3`                     | Green                   | Positive confirmations, approvals, and submits. Requires `custom_id`.     |
| `"danger"`    | `4`                     | Red                     | Destructive actions, cancellations, and kicks/bans. Requires `custom_id`. |
| `"link"`      | `5`                     | Grey with External Icon | External web navigation. Requires valid `url`; `custom_id` is forbidden.  |

### Button Definitions in Modules

#### Action Buttons

```javascript
const confirmButton = {
    type: 2,
    style: "success",
    label: this.medusa.modules.lang.phrase("example", "messages", "confirm"),
    emoji: "%custom_emoji_check%",
    custom_id: "medusa:button:core:confirm"
};

const cancelButton = {
    type: 2,
    style: "secondary",
    label: this.medusa.modules.lang.phrase("example", "messages", "cancel"),
    emoji: "%custom_emoji_cross%",
    custom_id: "medusa:button:core:cancel"
};
```

#### Link Button

```javascript
const websiteButton = {
    type: 2,
    style: "link",
    url: "https://zerodev.ca",
    label: "Zero Development",
    emoji: "%custom_emoji_external%"
};
```

#### Metric / Counter Button (Disabled)

```javascript
const counterButton = {
    type: 2,
    style: "secondary",
    label: this.medusa.modules.lang.phrase("anti-scam", "messages", "bannedButton", { count: String(bannedTotal) }),
    emoji: "%custom_emoji_hammer%",
    disabled: true,
    custom_id: "honeypot_banned_display"
};
```

***

## Select Menus (`type: 3`)

String Select Menus provide an interactive dropdown list allowing members to pick an item from up to 25 choices.

### Select Menu Schema

```typescript
interface SelectMenuOption {
    label: string;
    value: string;
    description?: string;
    emoji?: string | { id?: string; name: string; animated?: boolean };
    default?: boolean;
}

interface SelectMenuComponent {
    type: 3;
    custom_id?: string;
    placeholder?: string;
    disabled?: boolean;
    options: SelectMenuOption[];
}
```

| Field         | Type      | Description                                                                     |
| :------------ | :-------- | :------------------------------------------------------------------------------ |
| `type`        | `3`       | Discord component type for String Select Menus.                                 |
| `custom_id`   | `string`  | Event identifier. Defaults to `"select_menu"` if omitted.                       |
| `placeholder` | `string`  | Descriptive prompt displayed before an option is selected (max 150 characters). |
| `disabled`    | `boolean` | When `true`, prevents dropdown expansion and interactions.                      |
| `options`     | `Array`   | List of 1 to 25 select menu options.                                            |

### Select Menu Option Schema

| Field         | Type               | Description                                                                                          |
| :------------ | :----------------- | :--------------------------------------------------------------------------------------------------- |
| `label`       | `string`           | **Required.** Item title (max 100 characters). Auto-falls back to `Option N`.                        |
| `value`       | `string`           | **Required.** Unique value returned upon selection (max 100 characters). Auto-falls back to `opt_N`. |
| `description` | `string`           | Optional secondary description text displayed below the title (max 100 characters).                  |
| `emoji`       | `string \| object` | Custom bot emoji token (e.g. `%custom_emoji_bell%`), unicode character, or emoji object.             |
| `default`     | `boolean`          | When `true`, this option is pre-selected by default.                                                 |

### Select Menu Example

```javascript
const categoryMenu = {
    type: 3,
    custom_id: "medusa:select:help:category",
    placeholder: this.medusa.modules.lang.phrase("example", "messages", "selectCategory"),
    options: [
        {
            label: "Moderation",
            value: "moderation",
            description: "Server moderation and security utilities",
            emoji: "%custom_emoji_gavel%",
            default: false
        },
        {
            label: "Economy",
            value: "economy",
            description: "Currency, banking, and marketplace features",
            emoji: "%custom_emoji_coin%",
            default: false
        }
    ]
};
```

***

## Internal API (`this.medusa.embeds`)

Every building block (handlers, commands, events, routes) receives access to Medusa's internal Embeds service through `this.medusa.embeds`.

### Method Reference

| Method             | Signature                                                                                                                                                                                 | Description                                                                                                                                                                                                                                                                                                                                                                   |
| :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolveComponent` | `(comp: object, tokens?: object) => object`                                                                                                                                               | Recursively resolves an entire component tree: converts style string literals to Discord numbers, resolves `%custom_emoji_<name>%` placeholders, performs token string substitution, and normalizes hex accent colors (`"#ef4444"`).                                                                                                                                          |
| `buildComponents`  | `(buttons?: Array, selectMenu?: object, rawComponents?: Array) => Array`                                                                                                                  | Assembles button arrays into Action Rows in groups of 5 (max 5 rows) and appends select menus in dedicated rows.                                                                                                                                                                                                                                                              |
| `buttonStyleNum`   | `(style: string \| number) => number`                                                                                                                                                     | Converts style literals (`"link"` $\to 5$, `"secondary"` $\to 2$, `"success"` $\to 3$, `"danger"` $\to 4$, default $\to 1$).                                                                                                                                                                                                                                                  |
| `parseButtonEmoji` | `(token: any) => object \| null`                                                                                                                                                          | Resolves custom emoji tags, `%custom_emoji_<name>%` tokens, and Unicode characters into `{ id, name, animated }` or `{ name }`.                                                                                                                                                                                                                                               |
| `isComponentsV2`   | `(components: Array) => boolean`                                                                                                                                                          | Checks whether any component in an array uses V2 component types (`17`, `9`, `10`, `12`, `13`, `14`, `15`).                                                                                                                                                                                                                                                                   |
| `send`             | `(channelId: string, content?: string, template?: object, tokens?: object, buttons?: Array, selectMenu?: object, components?: Array, flags?: number, embeds?: Array) => Promise<Message>` | Dispatches a complete Discord message with automatic V1/V2 component resolution, token interpolation, and notification suppression.                                                                                                                                                                                                                                           |
| `saveMessage`      | `(message: object) => Promise<object>`                                                                                                                                                    | Persists a message template with buttons and components into settings storage (`"MESSAGES"`).                                                                                                                                                                                                                                                                                 |
| `deleteMessage`    | `(id: string) => Promise<Array<object>>`                                                                                                                                                  | Deletes a saved message template from storage by ID.                                                                                                                                                                                                                                                                                                                          |
| `messages`         | `() => Array<object>`                                                                                                                                                                     | Retrieves all saved message templates from settings storage.                                                                                                                                                                                                                                                                                                                  |
| `resolve`          | `(template: object, tokens?: object, options?: object) => EmbedBuilder`                                                                                                                   | Fills tokens and custom emojis in an embed template and returns a decorated `EmbedBuilder`. A token whose value is an array is a list token: `template.lists[token]` is filled once per array entry (each entry is its own token object) and the joined result replaces `%token%`. See [Embed Fields](/docs/medusa/developer-documentation/configuration#embed-fields-embed). |
| `fill`             | `(value: any, tokens?: object) => any`                                                                                                                                                    | Recursively interpolates `%token%` and `<token>` values and custom emojis across strings, arrays, and nested objects.                                                                                                                                                                                                                                                         |

***

### `resolveComponent(comp, tokens)`

The primary resolution pipeline for all components. It recursively handles all Discord component types:

1. **Type 17 & 15 (Container / Spacer)**:
   * Recursively resolves all child `components`.
   * Forces `accent_color` to the global `ACCENT_COLOUR` setting as a 24-bit decimal integer, ignoring any value set on the container.
2. **Type 9 (Section)**:
   * Recursively resolves child components.
   * Normalizes and resolves `accessory` components (Button `type: 2` or Thumbnail `type: 11`).
3. **Type 10 (Text Display)**:
   * Interpolates tokens inside `content`.
4. **Type 11 (Thumbnail)**:
   * Fills tokens in `media.url` and `description`.
5. **Type 12 (Media Gallery)**:
   * Iterates through `items`, resolving media URLs and descriptions.
6. **Type 14 (Separator)**:
   * Preserves divider and spacing layout rules.
7. **Type 1 (Action Row)**:
   * Resolves all child buttons (`type: 2`) by converting `style` via `buttonStyleNum`, resolving `emoji`, and substituting tokens in `label` and `url`.
   * Resolves all child select menus (`type: 3`) by filling `placeholder` and option `label`, `description`, and `emoji`.

```javascript
const resolvedContainer = this.medusa.embeds.resolveComponent({
    type: 17,
    accent_color: "#22c55e",
    components: [
        {
            type: 10,
            content: "Welcome to **%server_name%**!"
        },
        {
            type: 14,
            divider: true,
            spacing: 1
        },
        {
            type: 1,
            components: [
                {
                    type: 2,
                    style: "primary",
                    label: "Get Started",
                    emoji: "%custom_emoji_rocket%",
                    custom_id: "medusa:button:welcome:start"
                }
            ]
        }
    ]
}, { server_name: guild.name });
```

***

### `buildComponents(buttons, selectMenu, rawComponents)`

When constructing standard messages without manual row grouping, `buildComponents` packages button arrays automatically:

```javascript
const rows = this.medusa.embeds.buildComponents(
    [
        { type: 2, style: "secondary", label: "Previous", custom_id: "medusa:button:page:prev" },
        { type: 2, style: "secondary", label: "Next", custom_id: "medusa:button:page:next" }
    ],
    {
        custom_id: "medusa:select:page:jump",
        placeholder: "Jump to page...",
        options: [
            { label: "Page 1", value: "1" },
            { label: "Page 2", value: "2" }
        ]
    }
);
```

* Groups buttons into blocks of 5 per Action Row (up to 25 buttons total).
* Places the select menu into its own row following the button rows.
* Filters out broken link buttons lacking a valid URL.
* Auto-generates fallback `custom_id` values (`btn_1`, `btn_2`, etc.) if omitted on action buttons.

***

## Components V2 Layouts

Medusa natively supports modern Discord Components V2 cards. Build the card with `this.medusa.components` and dispatch it with `payload`, which applies `MessageFlags.IsComponentsV2` and resolves the tree.

```javascript
export class HoneypotHandler extends Handlers {
    button(bannedCount) {
        return this.medusa.components.button({
            style: "secondary",
            label: this.medusa.modules.lang.phrase("anti-scam", "messages", "bannedButton", { count: String(bannedCount) }),
            emoji: "%custom_emoji_hammer%",
            disabled: true,
            custom_id: "honeypot_banned_display"
        });
    }

    banner(bannedCount) {
        return this.medusa.components.container({
            accent: "#ef4444",
            components: [
                this.medusa.components.text(`### ${this.medusa.modules.lang.phrase("anti-scam", "messages", "bannerTitle")}\n\n${this.medusa.modules.lang.phrase("anti-scam", "messages", "bannerBody")}`),
                this.medusa.components.separator(),
                this.medusa.components.row([this.button(bannedCount)])
            ]
        });
    }

    async postBanner(channel, guild) {
        const bannedTotal = await this.count(guild.id);
        return channel.send(this.medusa.components.payload(this.banner(bannedTotal)));
    }
}
```

***

## Interaction Routing & Custom IDs

To prevent naming collisions between modules, Medusa enforces a namespaced pattern for all interaction custom IDs:

```
medusa:<component_type>:<module_name>:<action>[:parameters]
```

* **Buttons**: `medusa:button:<module>:<action>[:params]` (e.g. `medusa:button:emojilocker:menu:lock`, `medusa:button:ticket:claim:12345`).
* **Select Menus**: `medusa:select:<module>:<action>` (e.g. `medusa:select:roles:assign`).
* **Modals**: `medusa:modal:<module>:<action>` (e.g. `medusa:modal:ticket:reason`).

### Interaction Listener in Handlers

```javascript
export class TicketHandler extends Handlers {
    async handleInteraction(interaction) {
        if (!interaction.isButton() && !interaction.isStringSelectMenu()) return;

        const parts = interaction.customId.split(":");
        const scope = parts[0];
        const type = parts[1];
        const module = parts[2];
        const action = parts[3];

        if (scope !== "medusa" || module !== "ticket") return;

        switch (action) {
            case "close": {
                await interaction.update({
                    embeds: [this.medusa.embeds.warn(this.medusa.modules.lang.phrase("ticket", "messages", "ticketClosing"))],
                    components: []
                });
                break;
            }
            case "claim": {
                const ticketId = parts[4];
                await interaction.reply({
                    embeds: [this.medusa.embeds.success(this.medusa.modules.lang.phrase("ticket", "messages", "ticketClaimed", { id: ticketId }))],
                    flags: MessageFlags.Ephemeral
                });
                break;
            }
            default:
                break;
        }
    }
}
```

***

## Global Placeholders in Components

When components are resolved via `this.medusa.embeds.resolveComponent(comp, tokens)` or `this.medusa.embeds.send(...)`, the following tokens are automatically populated inside button labels, URLs, select menu options, and text contents:

| Token                            | Replacement Value                                         |
| :------------------------------- | :-------------------------------------------------------- |
| `%server%` / `%server_name%`     | Current Discord server name.                              |
| `%server_icon%` / `%guild_icon%` | Server icon URL.                                          |
| `%user_avatar%`                  | Interaction user or bot avatar URL.                       |
| `%bot_avatar%`                   | Discord application bot avatar URL.                       |
| `%brand_icon%`                   | Configured brand icon URL (`CUSTOM_ICON_URL`).            |
| `%name%`                         | Configured dashboard branding name (`DASHBOARD_NAME`).    |
| `%version%`                      | Active Medusa bot version.                                |
| `%custom_emoji_<name>%`          | Custom emoji string tag (e.g. `<:check:123456789>`).      |
| `<custom_token>`                 | Any custom key provided in the `tokens` object parameter. |
