Skip to main content

Why JSX for Discord Components?

CommandKit’s JSX component system provides a declarative way to build Discord UI components. Instead of chaining builder methods, you write components using familiar JSX syntax that’s easier to read and maintain.

Benefits

  • Declarative syntax - Components are self-documenting and easier to understand
  • Type-safe - Full TypeScript support with prop validation
  • Composable - Build reusable component patterns
  • Event handlers - Attach interaction handlers directly to components
  • Auto-cleanup - Automatic disposal of interaction collectors

How it Works

Traditional Builder Pattern

import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';

const button = new ButtonBuilder()
  .setCustomId('my-button')
  .setLabel('Click Me')
  .setStyle(ButtonStyle.Primary);

const row = new ActionRowBuilder()
  .addComponents(button);

await interaction.reply({
  components: [row]
});

JSX Component Pattern

import { ActionRow, Button } from 'commandkit';
import { ButtonStyle } from 'discord.js';

const row = (
  <ActionRow>
    <Button style={ButtonStyle.Primary}>Click Me</Button>
  </ActionRow>
);

await interaction.reply({
  components: [row]
});

Getting Started

Import Components

CommandKit exports JSX components from the main package:
import { 
  ActionRow,
  Button,
  Modal,
  ShortInput,
  ParagraphInput,
  StringSelectMenu,
  StringSelectMenuOption,
  UserSelectMenu,
  RoleSelectMenu,
  ChannelSelectMenu,
  MentionableSelectMenu
} from 'commandkit';

Configure JSX Runtime

Add to your tsconfig.json:
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "commandkit"
  }
}

Basic Example

Here’s a complete example showing buttons with click handlers:
import {
  Button,
  ActionRow,
  CommandData,
  OnButtonKitClick,
  ChatInputCommand,
} from 'commandkit';
import { ButtonStyle, MessageFlags } from 'discord.js';

export const command: CommandData = {
  name: 'confirm',
  description: 'Confirm an action',
};

const handleConfirm: OnButtonKitClick = async (interaction, context) => {
  await interaction.reply({
    content: 'Action confirmed!',
    flags: MessageFlags.Ephemeral,
  });
  
  // Clean up the interaction collector
  context.dispose();
};

const handleCancel: OnButtonKitClick = async (interaction, context) => {
  await interaction.reply({
    content: 'Action cancelled.',
    flags: MessageFlags.Ephemeral,
  });
  
  context.dispose();
};

export const chatInput: ChatInputCommand = async ({ interaction }) => {
  const buttons = (
    <ActionRow>
      <Button onClick={handleCancel} style={ButtonStyle.Secondary}>
        Cancel
      </Button>
      <Button onClick={handleConfirm} style={ButtonStyle.Danger}>
        Confirm
      </Button>
    </ActionRow>
  );

  await interaction.reply({
    content: 'Are you sure you want to delete this item?',
    components: [buttons],
  });
};

Component Reference

ActionRow

Container for interactive components. Required for buttons, select menus, and text inputs.
<ActionRow>
  {/* Add up to 5 buttons or 1 select menu */}
</ActionRow>
Props:
children
AnyCommandKitElement | AnyCommandKitElement[]
The components to display in this action row

Available Components

  • Buttons - Interactive buttons with click handlers
  • Modals - Forms with text inputs
  • Select Menus - Dropdowns for selecting options

Event Handlers

All interactive components support event handlers:

onClick (Buttons)

const handleClick: OnButtonKitClick = async (interaction, context) => {
  // Handle the button click
  await interaction.reply('Clicked!');
  
  // Clean up the collector
  context.dispose();
};

<Button onClick={handleClick}>Click Me</Button>

onSubmit (Modals)

const handleSubmit: OnModalKitSubmit = async (interaction, context) => {
  const value = interaction.fields.getTextInputValue('input-id');
  await interaction.reply(`You entered: ${value}`);
  
  context.dispose();
};

<Modal title="My Modal" onSubmit={handleSubmit}>
  <ShortInput customId="input-id" />
</Modal>

onSelect (Select Menus)

const handleSelect: OnStringSelectMenuKitSubmit = async (interaction, context) => {
  const values = interaction.values.join(', ');
  await interaction.reply(`Selected: ${values}`);
  
  context.dispose();
};

<StringSelectMenu onSelect={handleSelect}>
  <StringSelectMenuOption label="Option 1" value="1" />
  <StringSelectMenuOption label="Option 2" value="2" />
</StringSelectMenu>

Auto-Generated Custom IDs

When you attach event handlers, CommandKit automatically generates unique custom IDs:
// No customId needed - one is generated automatically
<Button onClick={handleClick}>Click Me</Button>

// Equivalent to:
<Button customId="buttonkit::a1b2c3d4-..." onClick={handleClick}>
  Click Me
</Button>
You can still provide your own customId if you need a specific value for tracking or persistence.

Interaction Collectors

CommandKit’s JSX components use an event interceptor system for handling interactions:
  • Auto-reset - Collectors automatically reset their timeout on each interaction
  • Configurable timeout - Default 5 minutes, customizable via options prop
  • Manual disposal - Call context.dispose() to clean up

Custom Collector Options

const options = {
  time: 10 * 60 * 1000, // 10 minutes
  autoReset: true, // Reset timer on each interaction
};

<Button onClick={handleClick} options={options}>
  Click Me
</Button>

Next Steps

Buttons

Learn about button styles and click handlers

Modals

Build forms with text inputs

Select Menus

Create dropdowns for user input

Build docs developers (and LLMs) love