Snaps
A Snap is a persistent handler for a Discord component interaction — buttons, select menus, modals. Each file in src/snaps/ registers by customId pattern rather than by per-message collector.
The key difference from collectors: Snaps are registered at startup and survive indefinitely. A user can click a button from last week and the Snap will still handle it. Collectors attached to a specific message expire when the bot restarts.
Basic button snap
Section titled “Basic button snap”import { snap } from "popii-framework";
export default snap({ customId: "confirm", async do(pop) { await pop.reply({ content: "Confirmed!", ephemeral: true }); },});Any button or component with customId: "confirm" triggers this handler — regardless of which message it’s on or when it was sent.
Dynamic IDs with regex
Section titled “Dynamic IDs with regex”Use a RegExp for IDs that carry data in their structure:
import { snap } from "popii-framework";
export default snap({ customId: /^delete:(\d+)$/, async do(pop) { const [, messageId] = pop.snapMatches!; // fetch and delete the message with messageId await pop.reply({ content: "Message deleted.", ephemeral: true }); },});pop.snapMatches is the result of customId.exec(interaction.customId).
Prefix matching
Section titled “Prefix matching”Simpler than regex when you just need a namespace:
import { snap } from "popii-framework";
export default snap({ prefix: "vote:", // matches "vote:yes", "vote:no", "vote:anything" async do(pop) { const choice = pop.interaction.customId.slice("vote:".length); await pop.reply(`You voted: **${choice}**`); },});Embedding structured data with pack/unpack
Section titled “Embedding structured data with pack/unpack”Discord’s customId field is capped at 100 characters — not enough for JSON. Use pop.pack() to encode a payload into a compact string, and read it back with pop.snapData in the handler:
// In a command — build a button with embedded contextconst row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setLabel("Ban") .setStyle(ButtonStyle.Danger) .setCustomId(pop.pack("mod:ban", { targetId: member.id, reason: "spam" })));await pop.reply({ components: [row], ephemeral: true });
// In the snap — read the context backexport default snap({ prefix: "mod:ban", permissions: ["BanMembers"], async do(pop) { const { targetId, reason } = pop.snapData as { targetId: string; reason: string }; await pop.guild?.members.ban(targetId, { reason }); await pop.reply({ content: "User banned.", ephemeral: true }); },});Select menus
Section titled “Select menus”import { snap } from "popii-framework";
export default snap({ customId: "role-picker", async do(pop) { const values = (pop.interaction as any).values as string[]; await pop.reply(`You selected: ${values.join(", ")}`); },});Modals
Section titled “Modals”import { snap } from "popii-framework";
export default snap({ customId: "feedback-modal", async do(pop) { const fields = (pop.interaction as any).fields; const feedback = fields.getTextInputValue("message"); // store or forward feedback await pop.reply({ content: "Thanks for the feedback!", ephemeral: true }); },});Expiring snaps
Section titled “Expiring snaps”Remove a handler automatically after a time window:
export default snap({ customId: "one-time-confirm", expiresIn: 300_000, // handler removed after 5 minutes async do(pop) { await pop.reply("Action confirmed."); },});Access control
Section titled “Access control”Snaps support the same guards as commands:
export default snap({ customId: "admin-panel", guildOnly: true, permissions: ["Administrator"], cooldown: 5_000, async do(pop) { // only guild Administrators can trigger this, once every 5 seconds },});