Skip to content

Modal Input Components

Discord introduced new interactive input components for modals beyond the original Text Input. These components enable richer form-like experiences with structured selection inputs.

Type Name Description Container
4 Text Input Free-form text entry (original modal input) Action Row
18 Label Wrapper component for new modal inputs
21 Radio Group Select exactly one option from a list Label
22 Checkbox Group Select one or many options from a list Label
23 Checkbox Single yes/no toggle Label
  • Label wrapper required: Radio Group, Checkbox Group, and Checkbox must be placed inside a Label component (type 18), not an Action Row. This is unlike Text Inputs which use Action Rows.
  • Modal-only: These components are only available in modals — they cannot be used in message payloads.
  • Submit data structure: The interaction response nests the input component inside the Label’s component field, not in an ActionRow.components array.

A Label is a container component for wrapping new modal input types. It provides a visible label and optional description above the input.

Labels are analogous to Action Rows for Text Inputs, but designed specifically for the newer input components.

Field Type Description
type integer 18 for label
id? integer Optional identifier for component
label string Text displayed above the input
description? string Optional description text displayed below label
component component The input component (Radio Group, Checkbox Group, or Checkbox)

A Radio Group allows the user to select exactly one option from a defined list. Useful for mutually exclusive choices like provider selection, mode switches, or preference settings.

Field Type Description
type integer 21 for radio group
id? integer Optional identifier for component
custom_id string Developer-defined identifier for the input; 1-100 characters
options array of radio group options List of options to show; min 2, max 10
required? boolean Whether a selection is required to submit the modal (default: true)
Field Type Description
value string Dev-defined value of the option; max 100 characters
label string User-facing label of the option; max 100 characters
description? string Optional description for the option; max 100 characters
default? boolean Shows the option as selected by default
Field Type Description
type integer 21 for a Radio Group
id integer Unique identifier for the component
custom_id string Developer-defined identifier for the input; 1-100 characters
value ?string The value of the selected option, or null if no option is selected
{
"type": 9,
"data": {
"custom_id": "class_selection_modal",
"title": "Class Selection",
"components": [
{
"type": 18,
"label": "Choose your class",
"description": "Your class determines the style of play for your character.",
"component": {
"type": 21,
"custom_id": "class_radio",
"options": [
{"value": "warrior", "label": "Warrior", "description": "Strong and brave"},
{"value": "rogue", "label": "Rogue", "description": "Weak and squishy"},
{"value": "wizard", "label": "Wizard", "description": "Nerd"},
{"value": "bard", "label": "Bard", "description": "Annoys everyone"},
{"value": "witch_doctor", "label": "Witch Doctor", "description": "Actually a pretty cool option"}
]
}
}
]
}
}
{
"type": 5,
"data": {
"custom_id": "class_selection_modal",
"components": [
{
"id": 1,
"type": 18,
"component": {
"custom_id": "class_radio",
"id": 2,
"type": 21,
"value": "warrior"
}
}
]
}
}

A Checkbox Group allows the user to select one or many options from a list. Ideal for multi-select scenarios like capability toggles, feature flags, or day-of-week selection.

Field Type Description
type integer 22 for checkbox group
id? integer Optional identifier for component
custom_id string Developer-defined identifier for the input; 1-100 characters
options array of checkbox group options List of options to show; min 1, max 10
min_values? integer Minimum items that must be chosen; min 0, max 10 (default: 1); if 0, required must be false
max_values? integer Maximum items that can be chosen; min 1, max 10 (default: number of options)
required? boolean Whether selecting within the group is required (default: true)
Field Type Description
value string Dev-defined value of the option; max 100 characters
label string User-facing label of the option; max 100 characters
description? string Optional description for the option; max 100 characters
default? boolean Shows the option as selected by default
Field Type Description
type integer 22 for a Checkbox Group
id integer Unique identifier for the component
custom_id string Developer-defined identifier for the input; 1-100 characters
values array of strings The values of the selected options, or [] if no options are selected
{
"type": 9,
"data": {
"custom_id": "day_selection_modal",
"title": "Study Days",
"components": [
{
"type": 18,
"label": "Which days are you free?",
"description": "Choose all of the days you're able to meet up.",
"component": {
"type": 22,
"custom_id": "event_checkbox",
"options": [
{"value": "march-4", "label": "March 4th"},
{"value": "march-5", "label": "March 5th"},
{"value": "march-7", "label": "March 7th", "description": "I know this is a Saturday and is tough"},
{"value": "march-9", "label": "March 9th"},
{"value": "march-10", "label": "March 10th"}
]
}
}
]
}
}
{
"type": 5,
"data": {
"custom_id": "day_selection_modal",
"components": [
{
"id": 1,
"type": 18,
"component": {
"custom_id": "event_checkbox",
"id": 2,
"type": 22,
"values": [
"march-5",
"march-10",
"march-4"
]
}
}
]
}
}

A Checkbox is a single toggle for simple yes/no questions. Unlike Checkbox Group (which provides a list of options), a standalone Checkbox is a single binary input.

Field Type Description
type integer 23 for checkbox
id? integer Optional identifier for component
custom_id string Developer-defined identifier for the input; 1-100 characters
default? boolean Whether the checkbox is selected by default

Note: Checkboxes cannot be set as required. To achieve required single-option behavior, use a Checkbox Group with one option and required: true.

Field Type Description
type integer 23 for a Checkbox
id integer Unique identifier for the component
custom_id string Developer-defined identifier for the input; 1-100 characters
value boolean The state of the checkbox (true if checked, false if unchecked)
{
"type": 9,
"data": {
"custom_id": "secret_note_modal",
"title": "Secret Note",
"components": [
{
"type": 18,
"label": "Do you like me?",
"component": {
"type": 23,
"custom_id": "like_checkbox"
}
}
]
}
}
{
"type": 5,
"data": {
"custom_id": "secret_note_modal",
"components": [
{
"id": 1,
"type": 18,
"component": {
"custom_id": "like_checkbox",
"id": 2,
"type": 23,
"value": true
}
}
]
}
}

Use this decision guide when choosing between modal input types.

Component Use When Avoid When
Text Input Free-form text entry: names, prompts, tags, API keys, URLs The input is a choice from a known set of options
String Select Large option sets (11+), dynamic/growing lists, options needing emoji or rich descriptions Small fixed set of mutually exclusive options (use Radio Group instead)
Radio Group Small fixed set of mutually exclusive options (2-10), unlikely to grow beyond 10 Option list is dynamic or may exceed 10 items (use String Select instead)
Checkbox Group Multiple items can be selected from a list (1-10 options), OR a single required yes/no toggle Mutually exclusive choice (use Radio Group)
Checkbox Single optional yes/no or on/off toggle question The answer is required (use Checkbox Group with 1 option instead)

Many TomoriBot modals include yes/no, enable/disable, or true/false string selects. These should be migrated to:

  • Optional booleanCheckbox: Unchecked submits as false, checked as true. The user can leave it unchecked and still submit.
  • Required booleanCheckbox Group with 1 option: Set required: true and provide a single option. This forces the user to explicitly check it before submitting — acting as a required confirmation or acknowledgment.
// Required boolean workaround: Checkbox Group with 1 option
{
"type": 18,
"label": "Enable this server?",
"component": {
"type": 22,
"custom_id": "enable_toggle",
"required": true,
"options": [
{"value": "true", "label": "Yes, enable"}
]
}
}
Is the input free-form text?
└─ Yes → Text Input
└─ No → Is it a single yes/no question?
└─ Yes → Is the answer required?
└─ Yes → Checkbox Group (1 option, required: true)
└─ No → Checkbox
└─ No → Can the user select multiple options?
└─ Yes → Checkbox Group (if ≤10 options)
└─ No → Is the option set small and fixed (≤10)?
└─ Yes → Radio Group
└─ No → String Select (supports 25+ via pagination)
  • Radio Group: 2-10 options. No emoji support. No placeholder text.
  • Checkbox Group: 1-10 options. Supports min_values/max_values for range control. Also serves as the workaround for required single-boolean inputs.
  • Checkbox: Cannot be required. Use a Checkbox Group with 1 option if required behavior is needed.
  • String Select: Up to 25 options natively, 25+ via promptWithPaginatedModal(). Supports emoji, descriptions, and placeholder text.
  • All new components must be wrapped in a Label (type 18), not an Action Row.

When a modal is editing an existing list of configured items, prefer Checkbox Groups over a one-at-a-time String Select when the full set fits in a single modal.

  • Pre-check every current entry and treat unchecked items as “remove” or “disable”.
  • Use min_values: 0 and required: false so users can submit with every item unchecked.
  • Chunk one category across multiple groups of 10 options, or split different entity types into separate groups.
  • Keep the first group descriptive and use “(Continued)” labels for later groups.
  • Respect Discord’s modal ceiling: 5 checkbox groups, 10 options each, 50 total entries.
  • If the list exceeds 50, warn clearly and fall back to a different management flow rather than silently truncating.
  • For persistent setting commands, treat checked items as the stored enabled-set and write the full checked set back on submit.

Implemented examples:

  • /server whitelist remove manages personas, channels, and roles in one modal.
  • /server private-channels manages the full saved private-channel set in one modal, with paginated fallback beyond 50 channels.
  • /server rp-channels manages the full saved RP-channel set in one modal, with paginated fallback beyond 50 channels.
  • /server crosschannel-blocklist manages a persistent channel blocklist with saved check states and paginated fallback beyond 50 channels.
  • /config notice-embeds visibility manages visible notice embed types in one modal.
  • /config remove modeloverride manages channel and persona overrides together in one modal.
  • /config mcp remove manages registered MCP servers in one modal.
  • /model fallback manages the fallback chain in one modal, and each slot can be cleared directly with the built-in None option.
  • /config random-trigger remove manages random triggers in one modal when the set fits, with paginated fallback beyond modal limits.
  • /server trigger remove manages trigger words for the selected persona in one modal when the set fits, with paginated fallback beyond modal limits.

A full survey of all modals in the codebase, categorized by migration eligibility.

These modals use a String Select with a small, fixed, mutually exclusive option set that is unlikely to grow beyond 10:

Command File Custom ID Current Input Options Why Radio Group
/config humanizer config/humanizer.ts humanizer_select String Select 4-5 (none/light/moderate/heavy; + inherit with scope: Persona) Fixed set of mutually exclusive degrees
/config setup config/setup.ts humanizer_degree String Select 4 (none/light/default/heavy) Same fixed humanizer degree set as above
/personal privacy personal/privacy.ts privacy_select String Select 3 (minimal/partial/full) Fixed set of 3 mutually exclusive levels
/generate image generate/image.ts aspect_ratio_select String Select 10 (1:1, 2:3, 3:2, 3:4, 4:3, etc.) Fixed set of 10 aspect ratios — at the limit
/config mcp add config/mcp/add.ts mcp_server_type String Select 3 (none/web_search/url_fetcher) Fixed set of 3 server types; optional field
/tool compact tool/compact.ts summary_type String Select 2 (conversation/roleplay) Fixed binary mode selection

Strong Candidates — Checkbox / Checkbox Group (Boolean Selects)

Section titled “Strong Candidates — Checkbox / Checkbox Group (Boolean Selects)”

These modals currently use a 2-option String Select (yes/no, true/false, enable/disable) that should become a Checkbox or Checkbox Group depending on whether the answer is required:

Command File Custom ID Current Options Required Migration Target
/config mcp toggle config/mcp/toggle.ts mcp_enabled_select Enable / Disable Yes Checkbox Group (1 option, required)
/config random-trigger add config/randomtrigger/add.ts respond_to_self Yes / No Yes Checkbox Group (1 option, required)
/tool compact tool/compact.ts refresh_context Yes / No Yes Checkbox Group (1 option, required)
/tool compact tool/compact.ts analyze_images Yes / No Yes Checkbox Group (1 option, required)
/config provider switch config/provider/switch.ts save_current_select Yes / No (default: Yes) No Checkbox (default: true, rarely unchecked)
/bot respond bot/respond.ts use_reasoning Yes / No No Checkbox (optional toggle)
/persona export persona/export.ts export_json_select False / True No Checkbox (optional toggle)

Note on /config provider switch: This modal has two migration candidates — the save-current-config toggle becomes a Checkbox (default checked, since users almost always want to save). The provider select itself is dynamic (loaded from DB via loadUniqueProviders()), so it stays as a String Select.

Note on /tool compact: This modal has three migration candidates — summary_type becomes a Radio Group, while refresh_context and analyze_images both become required Checkbox Groups.

Strong Candidates — Checkbox Group Bulk Management

Section titled “Strong Candidates — Checkbox Group Bulk Management”

These commands still remove one dynamic item at a time, but the data shape is a good fit for the unchecked-means-remove pattern:

Command File Current Input Why Checkbox Groups Fit Notes
/persona attribute remove persona/attribute/remove.ts Persona picker + single paginated select Personality attributes are usually reviewed and pruned in batches Needs index-safe array rewrite if duplicate attributes must be preserved
/scheduled-task remove scheduled-task/remove.ts Persona picker + single paginated select Reminder cleanup is often batch-oriented, especially for stale schedules Manager-only reminder views may need concise descriptions
/memory document remove memory/document/remove.ts Persona picker + single paginated select Document cleanup is an obvious multi-select management flow Large lists should keep paginated fallback
/memory history remove memory/history/remove.ts Persona picker + single paginated select History entries are frequently pruned in groups Large lists should keep paginated fallback
/persona sample-dialogue remove persona/sample-dialogue/remove.ts Persona picker + single paginated select Dialogue cleanup is often batch-oriented and already has index-safe removal Good fit for index-valued checkbox groups
/persona remove persona/remove.ts Single paginated select Alter persona cleanup could be batch-managed Should pair the bulk UI with stronger destructive-action messaging

These modals have dynamic or large option sets that exceed Radio Group/Checkbox Group limits:

Command File Reason
/config provider switch config/provider/switch.ts Provider list is dynamic (DB via loadUniqueProviders())
/model text config/model/text.ts Dynamic model list, often 25+, uses pagination
/model image config/model/image.ts Dynamic model list, uses pagination
/model vision config/model/vision.ts Dynamic model list, uses pagination
/model embedding config/model/embedding.ts Dynamic model list, uses pagination
/model fallback config/model/fallback.ts Dynamic model list, uses pagination
/config system-prompt preset config/system-prompt/preset.ts Dynamic preset list from DB
/config provider add config/provider/add.ts Provider select + text input combo; list may grow
/persona prompt set persona/prompt/set.ts Persona picker embed first, then a prefilled free-form prompt modal (up to 16000 chars, 4 fields)
/persona attribute add persona/attribute/add.ts Dynamic persona list, uses pagination
/persona sample-dialogue add persona/sample-dialogue/add.ts Dynamic persona list, uses pagination
/memory personal add memory/personal/add.ts Dynamic memory list
/memory server add memory/server/add.ts Dynamic memory list
/persona image-tags persona/image-tags.ts Persona picker first, then a prefilled free-form tag modal
/persona attribute remove persona/attribute/remove.ts Dynamic attribute list, uses pagination
/scheduled-task remove scheduled-task/remove.ts Dynamic reminder list
/server welcome-channel set server/welcome-channel/set.ts Channel option + dynamic persona list

These modals collect free-form text and have no structured option set:

Command File Reason
/config system-prompt set config/system-prompt/set.ts Free-form paragraph text (up to 16000 chars, 4 fields)
/config random-trigger add config/random-trigger/add.ts Free-form trigger word/phrase (text input portion stays)
/novelai attg novelai/attg.ts 5 free-form text fields (author, title, tags, etc.)
/personal image-tags personal/image-tags.ts Free-form physical appearance image tag text
/config image-tags default-negative config/image-tags/default-negative.ts Free-form default negative tag text
/config image-tags default-positive config/image-tags/default-positive.ts Free-form default positive tag text
/persona create persona/create.ts Free-form text fields + file upload
/persona generate persona/generate.ts Free-form name + file upload
/server trigger add server/trigger/add.ts Free-form text fields (word, response, cooldown)
/server avatar server/avatar.ts Persona select + optional file upload
/tool comment tool/comment.ts Free-form paragraph text
/memory personal import memory/personal/import.ts File upload only

When a flow needs both a selection modal and a later prefilled edit modal, use:

  1. selection modal
  2. confirmation embed with buttons
  3. showModal() from the confirm button interaction

This is the pattern used by the /memory personal edit, /memory server edit, /persona attribute edit, and /persona sample-dialogue edit flows.

For persona-scoped flows that already have a persistent ephemeral picker message, prefer replacing that same message with the confirmation embed and later success state instead of spawning a second ephemeral thread.

Do not use promptWithConfirmation() for this case. It eagerly deferUpdate()s the button click in its collector filter, which consumes the interaction and prevents the next showModal() call.

Use promptWithUnacknowledgedConfirmation() instead so the confirm button interaction stays available for the edit modal.


As of discord.js v14.x, these components may not yet have dedicated builder classes. TomoriBot already uses promptWithRawModal() in interactionHelper.ts which sends raw component payloads via the Discord REST API — this approach will work for the new component types without waiting for discord.js builder support. The raw modal system already handles Label (type 18) wrapping for string selects and file uploads, so extending it to support Radio Group (type 21), Checkbox Group (type 22), and Checkbox (type 23) should be straightforward.