Components
Athena ships two builders: ComponentBuilder for classic components (V1) and NewComponentBuilder for Components V2. Use NewComponentBuilder for anything...
Athena ships two builders: ComponentBuilder for classic components (V1) and NewComponentBuilder for Components V2. Use NewComponentBuilder for anything new; it supports containers, sections, separators, media galleries, and files alongside the classic rows.
Classic components (V1)
Up to five action rows, attached as components.
import { ComponentBuilder, ButtonStyle } from 'athena';
const components = new ComponentBuilder().addActionRow((row) =>
row
.addNormalButton({ custom_id: 'confirm', style: ButtonStyle.Success, label: 'Confirm' })
.addNormalButton({ custom_id: 'cancel', style: ButtonStyle.Danger, label: 'Cancel' })
);
await client.createMessage(channelID, { content: 'Proceed?', components: components.toJSON() });Pass a prefix to namespace custom IDs, which the framework uses for routing:
new ComponentBuilder('ticket').addActionRow((row) =>
row.addNormalButton({ custom_id: 'close', style: ButtonStyle.Danger, label: 'Close' })
); // custom_id becomes "ticket:close"Limits: 5 rows per message, 5 buttons per row, 1 select per row (no mixing with buttons).
Components V2
Richer layouts. Send with toMessageData(), which sets the V2 flag and omits fields V2 messages cannot carry (content, embeds, sticker_ids, poll).
import { NewComponentBuilder, ButtonStyle } from 'athena';
const components = new NewComponentBuilder().addContainer((c) =>
c
.setAccentColor(0x5865f2)
.addTextDisplay('# Welcome')
.addSeparator({ spacing: 2 })
.addActionRow((row) => row.addNormalButton({ custom_id: 'role:gamer', style: ButtonStyle.Primary, label: 'Gamer' }))
);
await client.createMessage(channelID, components.toMessageData());V2 primitives
addTextDisplay(content, id?)- markdown text.addSeparator({ spacing: 1 | 2, divider? })- a rule or spacer.addSection(cb)- up to 3 text displays plus one accessory (setAccessoryAsNormalButton,setAccessoryAsURLButton,setAccessoryAsPremiumButton,setAccessoryAsThumbnail).addMediaGallery(cb)- up to 10 media items.addFile(media, options?)- an inline file reference.addContainer(cb)- a panel with optional accent colour and spoiler, holding any of the above.addActionRow(cb)- same buttons/selects as V1.
Limits: 40 components total per message; 5 per row; 1 select per row; 3 text displays per section (a section requires an accessory); 10 items per gallery.
Buttons
row.addNormalButton({ custom_id: 'click', style: ButtonStyle.Primary, label: 'Click', emoji: 'thumbsup', disabled: false });
row.addURLButton({ url: 'https://example.com', label: 'Open' });
row.addPremiumButton({ sku_id: '123', disabled: false });Emoji accepts a unicode character or <:name:id> / <a:name:id>. URL and premium buttons have no custom_id.
Select menus
row.addStringSelect({ custom_id: 'pick', placeholder: 'Choose', min_values: 1, max_values: 1, options: [
{ label: 'Vanilla', value: 'vanilla', default: true },
{ label: 'Chocolate', value: 'chocolate' }
]});
row.addChannelSelect({ custom_id: 'ch', channel_types: [ChannelType.GuildText], default_values: ['123'] });
row.addRoleSelect({ custom_id: 'role' });
row.addUserSelect({ custom_id: 'user' });
row.addMentionableSelect({ custom_id: 'target' });default_values is an array of IDs; the builder wraps them correctly.
Handling component interactions
client.on('interactionCreate', async (interaction) => {
if (!interaction.isComponent()) return;
if (interaction.isButton() && interaction.data.custom_id === 'confirm') {
await interaction.deferUpdate();
await interaction.editParent({ content: 'Confirmed.' });
}
if (interaction.isStringSelect()) {
const picked = interaction.selected; // string[]
}
});With the commands framework, implement handleComponent on your Command and route by custom_id prefix instead of wiring interactionCreate.
Tips
- Prefix custom IDs by feature to avoid collisions and to route in the framework.
- Do not mix V1 and V2 on the same message; the V2 flag is sticky.