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.

TypeNameDescriptionContainer
4Text InputFree-form text entry (original modal input)Action Row
18LabelWrapper component for new modal inputs
21Radio GroupSelect exactly one option from a listLabel
22Checkbox GroupSelect one or many options from a listLabel
23CheckboxSingle yes/no toggleLabel
  • 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.

FieldTypeDescription
typeinteger18 for label
id?integerOptional identifier for component
labelstringText displayed above the input
description?stringOptional description text displayed below label
componentcomponentThe 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.

FieldTypeDescription
typeinteger21 for radio group
id?integerOptional identifier for component
custom_idstringDeveloper-defined identifier for the input; 1-100 characters
optionsarray of radio group optionsList of options to show; min 2, max 10
required?booleanWhether a selection is required to submit the modal (default: true)
FieldTypeDescription
valuestringDev-defined value of the option; max 100 characters
labelstringUser-facing label of the option; max 100 characters
description?stringOptional description for the option; max 100 characters
default?booleanShows the option as selected by default
FieldTypeDescription
typeinteger21 for a Radio Group
idintegerUnique identifier for the component
custom_idstringDeveloper-defined identifier for the input; 1-100 characters
value?stringThe 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.

FieldTypeDescription
typeinteger22 for checkbox group
id?integerOptional identifier for component
custom_idstringDeveloper-defined identifier for the input; 1-100 characters
optionsarray of checkbox group optionsList of options to show; min 1, max 10
min_values?integerMinimum items that must be chosen; min 0, max 10 (default: 1); if 0, required must be false
max_values?integerMaximum items that can be chosen; min 1, max 10 (default: number of options)
required?booleanWhether selecting within the group is required (default: true)
FieldTypeDescription
valuestringDev-defined value of the option; max 100 characters
labelstringUser-facing label of the option; max 100 characters
description?stringOptional description for the option; max 100 characters
default?booleanShows the option as selected by default
FieldTypeDescription
typeinteger22 for a Checkbox Group
idintegerUnique identifier for the component
custom_idstringDeveloper-defined identifier for the input; 1-100 characters
valuesarray of stringsThe 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.

FieldTypeDescription
typeinteger23 for checkbox
id?integerOptional identifier for component
custom_idstringDeveloper-defined identifier for the input; 1-100 characters
default?booleanWhether 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.

FieldTypeDescription
typeinteger23 for a Checkbox
idintegerUnique identifier for the component
custom_idstringDeveloper-defined identifier for the input; 1-100 characters
valuebooleanThe 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.

ComponentUse WhenAvoid When
Text InputFree-form text entry: names, prompts, tags, API keys, URLsThe input is a choice from a known set of options
String SelectLarge option sets (11+), dynamic/growing lists, options needing emoji or rich descriptionsSmall fixed set of mutually exclusive options (use Radio Group instead)
Radio GroupSmall fixed set of mutually exclusive options (2-10), unlikely to grow beyond 10Option list is dynamic or may exceed 10 items (use String Select instead)
Checkbox GroupMultiple items can be selected from a list (1-10 options), OR a single required yes/no toggleMutually exclusive choice (use Radio Group)
CheckboxSingle optional yes/no or on/off toggle questionThe 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:

CommandFileCustom IDCurrent InputOptionsWhy Radio Group
/config humanizerconfig/humanizer.tshumanizer_selectString Select4 (none/light/moderate/heavy)Fixed set of 4 mutually exclusive degrees
/config setupconfig/setup.tshumanizer_degreeString Select4 (none/light/default/heavy)Same fixed humanizer degree set as above
/personal privacypersonal/privacy.tsprivacy_selectString Select3 (minimal/partial/full)Fixed set of 3 mutually exclusive levels
/generate imagegenerate/image.tsaspect_ratio_selectString Select10 (1:1, 2:3, 3:2, 3:4, 4:3, etc.)Fixed set of 10 aspect ratios — at the limit
/config mcp addconfig/mcp/add.tsmcp_server_typeString Select3 (none/web_search/url_fetcher)Fixed set of 3 server types; optional field
/tool compacttool/compact.tssummary_typeString Select2 (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:

CommandFileCustom IDCurrent OptionsRequiredMigration Target
/config mcp toggleconfig/mcp/toggle.tsmcp_enabled_selectEnable / DisableYesCheckbox Group (1 option, required)
/config random-trigger addconfig/randomtrigger/add.tsrespond_to_selfYes / NoYesCheckbox Group (1 option, required)
/tool compacttool/compact.tsrefresh_contextYes / NoYesCheckbox Group (1 option, required)
/tool compacttool/compact.tsanalyze_imagesYes / NoYesCheckbox Group (1 option, required)
/config provider switchconfig/provider/switch.tssave_current_selectYes / No (default: Yes)NoCheckbox (default: true, rarely unchecked)
/bot respondbot/respond.tsuse_reasoningYes / NoNoCheckbox (optional toggle)
/persona exportpersona/export.tsexport_json_selectFalse / TrueNoCheckbox (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:

CommandFileCurrent InputWhy Checkbox Groups FitNotes
/persona attribute removepersona/attribute/remove.tsPersona picker + single paginated selectPersonality attributes are usually reviewed and pruned in batchesNeeds index-safe array rewrite if duplicate attributes must be preserved
/scheduled-task removescheduled-task/remove.tsPersona picker + single paginated selectReminder cleanup is often batch-oriented, especially for stale schedulesManager-only reminder views may need concise descriptions
/memory document removememory/document/remove.tsPersona picker + single paginated selectDocument cleanup is an obvious multi-select management flowLarge lists should keep paginated fallback
/memory history removememory/history/remove.tsPersona picker + single paginated selectHistory entries are frequently pruned in groupsLarge lists should keep paginated fallback
/persona sample-dialogue removepersona/sample-dialogue/remove.tsPersona picker + single paginated selectDialogue cleanup is often batch-oriented and already has index-safe removalGood fit for index-valued checkbox groups
/persona removepersona/remove.tsSingle paginated selectAlter persona cleanup could be batch-managedShould pair the bulk UI with stronger destructive-action messaging

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

CommandFileReason
/config provider switchconfig/provider/switch.tsProvider list is dynamic (DB via loadUniqueProviders())
/model textconfig/model/text.tsDynamic model list, often 25+, uses pagination
/model imageconfig/model/image.tsDynamic model list, uses pagination
/model visionconfig/model/vision.tsDynamic model list, uses pagination
/model embeddingconfig/model/embedding.tsDynamic model list, uses pagination
/model fallbackconfig/model/fallback.tsDynamic model list, uses pagination
/config system-prompt presetconfig/system-prompt/preset.tsDynamic preset list from DB
/config provider addconfig/provider/add.tsProvider select + text input combo; list may grow
/persona prompt setpersona/prompt/set.tsPersona picker embed first, then a prefilled free-form prompt modal (up to 16000 chars, 4 fields)
/persona attribute addpersona/attribute/add.tsDynamic persona list, uses pagination
/persona sample-dialogue addpersona/sample-dialogue/add.tsDynamic persona list, uses pagination
/memory personal addmemory/personal/add.tsDynamic memory list
/memory server addmemory/server/add.tsDynamic memory list
/persona image-tagspersona/image-tags.tsPersona picker first, then a prefilled free-form tag modal
/persona attribute removepersona/attribute/remove.tsDynamic attribute list, uses pagination
/scheduled-task removescheduled-task/remove.tsDynamic reminder list
/server welcome-channel setserver/welcome-channel/set.tsChannel option + dynamic persona list

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

CommandFileReason
/config system-prompt setconfig/system-prompt/set.tsFree-form paragraph text (up to 16000 chars, 4 fields)
/config random-trigger addconfig/random-trigger/add.tsFree-form trigger word/phrase (text input portion stays)
/novelai attgnovelai/attg.ts5 free-form text fields (author, title, tags, etc.)
/personal image-tagspersonal/image-tags.tsFree-form physical appearance image tag text
/config image-tags default-negativeconfig/image-tags/default-negative.tsFree-form default negative tag text
/config image-tags default-positiveconfig/image-tags/default-positive.tsFree-form default positive tag text
/persona createpersona/create.tsFree-form text fields + file upload
/persona generatepersona/generate.tsFree-form name + file upload
/server trigger addserver/trigger/add.tsFree-form text fields (word, response, cooldown)
/server avatarserver/avatar.tsPersona select + optional file upload
/tool commenttool/comment.tsFree-form paragraph text
/memory personal importmemory/personal/import.tsFile 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.