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.
Overview
Section titled “Overview”| 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 |
Key Differences from Message Components
Section titled “Key Differences from Message Components”- 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
componentfield, not in anActionRow.componentsarray.
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.
Label Structure
Section titled “Label Structure”| 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) |
Radio Group
Section titled “Radio Group”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.
Radio Group Structure
Section titled “Radio Group Structure”| 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) |
Radio Group Option Structure
Section titled “Radio Group Option Structure”| 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 |
Radio Group Interaction Response
Section titled “Radio Group Interaction Response”| 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 |
Modal Payload Example
Section titled “Modal Payload Example”{ "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"} ] } } ] }}Submit Interaction Data Example
Section titled “Submit Interaction Data Example”{ "type": 5, "data": { "custom_id": "class_selection_modal", "components": [ { "id": 1, "type": 18, "component": { "custom_id": "class_radio", "id": 2, "type": 21, "value": "warrior" } } ] }}Checkbox Group
Section titled “Checkbox Group”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.
Checkbox Group Structure
Section titled “Checkbox Group Structure”| 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) |
Checkbox Group Option Structure
Section titled “Checkbox Group Option Structure”| 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 |
Checkbox Group Interaction Response
Section titled “Checkbox Group Interaction Response”| 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 |
Modal Payload Example
Section titled “Modal Payload Example”{ "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"} ] } } ] }}Submit Interaction Data Example
Section titled “Submit Interaction Data Example”{ "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" ] } } ] }}Checkbox
Section titled “Checkbox”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.
Checkbox Structure
Section titled “Checkbox Structure”| 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 andrequired: true.
Checkbox Interaction Response
Section titled “Checkbox Interaction Response”| 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) |
Modal Payload Example
Section titled “Modal Payload Example”{ "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" } } ] }}Submit Interaction Data Example
Section titled “Submit Interaction Data Example”{ "type": 5, "data": { "custom_id": "secret_note_modal", "components": [ { "id": 1, "type": 18, "component": { "custom_id": "like_checkbox", "id": 2, "type": 23, "value": true } } ] }}Component Selection Standards
Section titled “Component Selection Standards”Use this decision guide when choosing between modal input types.
When to Use Each Component
Section titled “When to Use Each Component”| 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) |
Boolean Input Pattern
Section titled “Boolean Input Pattern”Many TomoriBot modals include yes/no, enable/disable, or true/false string selects. These should be migrated to:
- Optional boolean → Checkbox: Unchecked submits as
false, checked astrue. The user can leave it unchecked and still submit. - Required boolean → Checkbox Group with 1 option: Set
required: trueand 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"} ] }}Decision Flowchart
Section titled “Decision Flowchart”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)Key Constraints
Section titled “Key Constraints”- Radio Group: 2-10 options. No emoji support. No placeholder text.
- Checkbox Group: 1-10 options. Supports
min_values/max_valuesfor 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.
Bulk Configuration Management Pattern
Section titled “Bulk Configuration Management Pattern”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: 0andrequired: falseso 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 removemanages personas, channels, and roles in one modal./server private-channelsmanages the full saved private-channel set in one modal, with paginated fallback beyond 50 channels./server rp-channelsmanages the full saved RP-channel set in one modal, with paginated fallback beyond 50 channels./server crosschannel-blocklistmanages a persistent channel blocklist with saved check states and paginated fallback beyond 50 channels./config notice-embeds visibilitymanages visible notice embed types in one modal./config remove modeloverridemanages channel and persona overrides together in one modal./config mcp removemanages registered MCP servers in one modal./model fallbackmanages the fallback chain in one modal, and each slot can be cleared directly with the built-inNoneoption./config random-trigger removemanages random triggers in one modal when the set fits, with paginated fallback beyond modal limits./server trigger removemanages trigger words for the selected persona in one modal when the set fits, with paginated fallback beyond modal limits.
TomoriBot Migration Audit
Section titled “TomoriBot Migration Audit”A full survey of all modals in the codebase, categorized by migration eligibility.
Strong Candidates — Radio Group
Section titled “Strong Candidates — Radio Group”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 (none/light/moderate/heavy) | Fixed set of 4 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 vialoadUniqueProviders()), so it stays as a String Select.
Note on
/tool compact: This modal has three migration candidates —summary_typebecomes a Radio Group, whilerefresh_contextandanalyze_imagesboth 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 |
Not Candidates — Keep String Select
Section titled “Not Candidates — Keep String Select”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 |
Not Candidates — Keep Text Input
Section titled “Not Candidates — Keep Text Input”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 |
Button-To-Modal Confirmation Pattern
Section titled “Button-To-Modal Confirmation Pattern”When a flow needs both a selection modal and a later prefilled edit modal, use:
- selection modal
- confirmation embed with buttons
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.
discord.js Support Status
Section titled “discord.js Support Status”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.