Lightweight helper library for discord.js v14 to:
- listen for Modal Submit interactions via the raw gateway event,
- emit a typed
client.on("modalSubmit", ...)event, - provide a set of Modal v2 builders (Label-based format),
- simplify replying / deferring / updating and showing modals.
⚠️ discord.js v14 only.
- ✅
init(client)wires rawINTERACTION_CREATE→ emitsmodalSubmit - ✅
ModalSubmitInteractionwrapper:fields,selectMenus, andcomponents(supports legacy + new payloads)- helpers:
getTextInputValue,getField,getSelectMenuValues,getSelectMenu - response helpers mixin:
reply,deferReply,editReply,followUp,update,showModal
- ✅ Modal builders:
ModalModalLabelTextInputComponentSelectMenuComponent
- 🧩 Backward-compat:
- accepts raw/legacy modal components and tries to normalize to
Label ModalActionRowis kept for compatibility, but deprecated
- accepts raw/legacy modal components and tries to normalize to
npm i @avion-block/discord-modalsPeer deps:
discord.jsv14
import { Client, GatewayIntentBits } from "discord.js";
import initModals from "@avion-block/discord-modals";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// Attach the raw interaction listener
initModals(client);
client.login(process.env.DISCORD_TOKEN);client.on("modalSubmit", async (modal) => {
const name = modal.getTextInputValue("name");
const roleIds = modal.getSelectMenuValues("roles") ?? [];
await modal.reply({
content: `Name: ${name ?? "(empty)"}\nRoles: ${roleIds.join(", ") || "(none)"}`,
ephemeral: true,
});
});You can show a modal using:
modal.showModal(modalBuilderOrJson)(available on the interaction wrapper)- or
showModal(modal, { client, interaction })
import {
Modal,
ModalLabel,
TextInputComponent,
} from "@avion-block/discord-modals";
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName !== "form") return;
const modal = new Modal()
.setCustomId("my_form")
.setTitle("Example form")
.addLabelComponents(
new ModalLabel().setLabel("Your name").setComponent(
new TextInputComponent()
.setCustomId("name")
.setLabel("Name") // used for inference / compatibility
.setPlaceholder("Type your name...")
.setRequired(true),
),
);
// If you already use this library's InteractionResponses mixin, you can:
// await (interaction as any).showModal(modal);
//
// Otherwise use the exported helper:
const { showModal } = await import("@avion-block/discord-modals");
await showModal(modal, { client: interaction.client, interaction });
});import { showModal } from "@avion-block/discord-modals";
await showModal(
{
title: "Raw modal",
custom_id: "raw_modal",
components: [
{
type: 18,
label: "Email",
component: {
type: 4,
custom_id: "email",
label: "Email",
style: 1,
required: true,
},
},
],
},
{ client, interaction },
);const modal = new Modal()
.setCustomId("settings")
.setTitle("Settings")
.addComponents(
// You can pass raw components: they’ll be wrapped into ModalLabel automatically.
new TextInputComponent().setCustomId("bio").setLabel("Bio"),
);Notes:
- Internally
Modal.componentsstoresModalLabel[](Modal v2 format). - If you add a raw component, the library wraps it in a label and tries to infer label text
from
component.labelorcomponent.placeholder. If it can’t infer, it falls back to"Field".
const label = new ModalLabel()
.setLabel("Username")
.setDescription("This will be displayed publicly.")
.setComponent(
new TextInputComponent().setCustomId("username").setLabel("Username"),
);import { TextInputStyle } from "discord-api-types/v10";
const input = new TextInputComponent()
.setCustomId("feedback")
.setLabel("Feedback")
.setStyle(TextInputStyle.Paragraph) // or "LONG" / 2
.setMinLength(10)
.setMaxLength(500)
.setPlaceholder("Write your feedback...")
.setRequired(true);const select = new SelectMenuComponent()
.setCustomId("color")
.setPlaceholder("Pick a color")
.setMinValues(1)
.setMaxValues(1)
.addOptions(
{ label: "Red", value: "red" },
{ label: "Green", value: "green" },
);Tip: for label inference on selects, make sure to set
placeholder(or set label explicitly).
Inside client.on("modalSubmit", modal => ...):
const value = modal.getTextInputValue("name"); // string | null
const field = modal.getField("name"); // ModalSubmitField | nullconst values = modal.getSelectMenuValues("roles"); // string[] | null
const menu = modal.getSelectMenu("roles"); // ModalSubmitSelectMenu | nullmodal.fields: ModalSubmitField[](type 4 components)modal.selectMenus: ModalSubmitSelectMenu[](select components)modal.components: (ModalLabel | any)[]- wraps Label (type 18) into
ModalLabelwhen possible - keeps unknown/legacy structures as raw
any
- wraps Label (type 18) into
ModalSubmitInteraction includes response helpers:
await modal.deferReply({ ephemeral: true });
await modal.editReply({ content: "Updated!" });
await modal.followUp({ content: "Follow up message" });Also supports:
reply,fetchReply,deleteReplydeferUpdate,update(for message-component interactions)
Discord Modal v2 uses Label (type 18), not ActionRow.
ModalActionRow exists only for backward compatibility.
import { ModalActionRow } from "@avion-block/discord-modals";
/\*\*
- @deprecated Prefer ModalLabel + component builders.
\*/
const row = new ModalActionRow().addComponent(
new TextInputComponent().setCustomId("x").setLabel("X"),
);Util.verifyString(...)Util.parseEmoji(...)Util.resolvePartialEmoji(...)SnowflakeUtil.generate(...),SnowflakeUtil.timestampFrom(...), etc.
The library augments discord.js Client with:
client.on("modalSubmit", (modal) => { ... });MIT