npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nexcord/tsx

v0.0.1

Published

<div align="center">

Readme

@nexcord/tsx

Write discord.js message components — buttons, selects, modals, embeds, and Components V2 layouts — as JSX.

npm version license discord.js status

Part of the Nexcord ecosystem.


@nexcord/tsx is a JSX runtime for discord.js. Instead of chaining new ButtonBuilder().setCustomId(...).setLabel(...), you write:

<button id="confirm_yes" label="Confirm" style="success" action={ConfirmAction} />

…and it compiles directly to the real ButtonBuilder instance discord.js expects — no virtual DOM, no diffing, just JSX as syntax sugar over the builder API you already know. Interactive elements (buttons, selects, modals) also wire themselves up to Nexcord's dependency injection cache, so handling the resulting interaction is a matter of pointing action at a class.

Table of Contents

Requirements

  • @nexcord/core — action controllers are resolved through its decorator cache, and interaction listeners are attached to its underlying discord.js client.
  • discord.js ^14.26.4 or later — this is the release line that introduced the modal Label field system (LabelBuilder, CheckboxBuilder, CheckboxGroupBuilder, RadioGroupBuilder, FileUploadBuilder) and Components V2 (ContainerBuilder, SectionBuilder, etc.), both of which @nexcord/tsx targets directly.
  • TypeScript, with the JSX configuration described below.

Installation

bun add @nexcord/tsx @nexcord/core discord.js
npm install @nexcord/tsx @nexcord/core discord.js
pnpm add @nexcord/tsx @nexcord/core discord.js
yarn add @nexcord/tsx @nexcord/core discord.js

tsconfig.json

@nexcord/tsx uses TypeScript's classic JSX transform with a custom pragma, so your tsconfig.json needs the following, exactly:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "jsx": "react",
    "jsxFactory": "DiscordJSX.createElement",
    "jsxFragmentFactory": "DiscordJSX.Fragment"
  }
}

Because the classic transform doesn't auto-import a pragma, every .tsx file that uses JSX must import DiscordJSX itself, even if you never reference the name directly — TypeScript needs it in scope to compile <tag /> into DiscordJSX.createElement('tag', ...):

import { DiscordJSX } from '@nexcord/tsx';

Quick Start

Define a component that returns JSX:

// src/components/confirm.components.tsx
import { DiscordJSX } from '@nexcord/tsx';
import { ConfirmAction } from '../common/actions/Confirm.action';

export function ConfirmButtons() {
  return (
    <row>
      <button id="confirm_yes" label="Confirm" style="success" action={ConfirmAction} />
      <button id="confirm_no" label="Cancel" style="danger" action={ConfirmAction} />
    </row>
  );
}

Handle the click with a controller class — a regular @Listener() with an execute method:

// src/common/actions/Confirm.action.ts
import { Listener } from '@nexcord/core';
import type { ButtonInteraction } from 'discord.js';

@Listener()
export class ConfirmAction {
  async execute(interaction: ButtonInteraction): Promise<void> {
    const confirmed = interaction.customId === 'confirm_yes';
    await interaction.update({
      content: confirmed ? '✅ Confirmed.' : '❌ Cancelled.',
      components: []
    });
  }
}

And send it like any other discord.js component array:

await interaction.reply({ content: 'Are you sure?', components: [ConfirmButtons()] });

How It Works

@nexcord/tsx exports a single pragma object, DiscordJSX, with two functions: createElement and Fragment. When TypeScript compiles your JSX, it calls these directly — there's no build-time codegen beyond what tsc (or Bun) already does.

  • Intrinsic elements (lowercase tags like <button>, <embed>, <container>) are matched against a big switch and turned into the matching discord.js builder instance, fully configured from your props and children.
  • Function components (components you write, like ConfirmButtons above) simply receive { ...props, children } as their only argument — this is plain JSX composition, nothing Discord-specific.
  • Interactive elements<button>, the five select menus, and <modal> — additionally register a global interactionCreate listener on NexCord.client the moment they're built, matching on the element's id and delegating to whatever you passed as action.

There is no virtual DOM and no re-rendering: calling a component function produces a builder tree once, synchronously.

Element Reference

Interactive components (messages)

| Element | Produces | Key props | | --- | --- | --- | | <row> | ActionRowBuilder | wraps buttons or a single select menu | | <button> | ButtonBuilder | id, label, style? (primary | secondary | success | danger | link), disabled?, url?, emoji?, action? | | <stringSelect> | StringSelectMenuBuilder | id, placeholder?, min?, max?, disable?, required?, action? — children: <option> | | <userSelect> / <roleSelect> / <mentionableSelect> | respective *SelectMenuBuilder | id, placeholder?, min?, max?, disable?, required?, action? | | <channelSelect> | ChannelSelectMenuBuilder | as above, plus types? (ChannelType[]) and defaults? | | <option> | plain option object | label, value, description? — used inside selects, <checkboxGroup>, and <radioGroup> |

Modal & form field components

| Element | Produces | Notes | | --- | --- | --- | | <modal> | ModalBuilder | id, title, action? — children: <label> (recommended), <row> (legacy text input), <text> | | <label> | LabelBuilder | label, description? — wraps exactly one field element below with a heading | | <input> | TextInputBuilder | id, label?, style?, min?, max?, placeholder?, required? | | <checkbox> | CheckboxBuilder | id, default? | | <checkboxGroup> | CheckboxGroupBuilder | id, min?, max?, required? — children: <option> | | <radioGroup> | RadioGroupBuilder | id, required? — children: <option> | | <fileUpload> | FileUploadBuilder | id, min?, max?, required? |

Components V2 / display components

Discord's newer layout system for messages — see Example: A Components V2 Layout for the message-flag requirement.

| Element | Produces | Notes | | --- | --- | --- | | <container> | ContainerBuilder | accentColor?, spoiler? — children: <row>, <text>, <split>, <section>, <file> | | <section> | SectionBuilder | children: one or more <text>, plus one <button> or <thumbnail> as an accessory | | <text> | TextDisplayBuilder | plain markdown content, taken from children | | <split> | SeparatorBuilder | divider?, spacing? | | <thumbnail> | ThumbnailBuilder | url, description?, spoiler? | | <file> | FileBuilder | url, spoiler? |

Embeds

| Element | Produces | Notes | | --- | --- | --- | | <embed> | EmbedBuilder | title, description, color? (defaults to a random color), author?, footer?, thumbnail?, image?, url?, timestamp? — children: <field> | | <field> | embed field object | name, inline? — children become the field's value text |

The action Prop & Controller Contract

Any <button>, select menu, or <modal> accepts an action prop — a single class, or an array of classes. Each one must be decorated with @Listener() from @nexcord/core (the same registration used for event listeners) and expose an execute(...) method. When the matching interaction comes in, @nexcord/tsx resolves the class from Nexcord's decorator cache and invokes it for you:

| Triggering element | execute is called as | | --- | --- | | <button>, any select menu | execute(interaction, properties) | | <modal> | execute(interaction, inputs, properties) |

  • properties is the element's own JSX props (id, label, action, …).
  • inputs, modal-only, is a plain object keyed by each field's id, already parsed by type: select menus resolve to their selected values, checkboxes to booleans, text inputs to strings, file uploads to the uploaded files, and role/user/channel/mentionable selects to their resolved Discord objects.
  • If action is an array, each controller's execute runs in order, one after another.

Example: A Modal with Field Extraction

// src/components/support.components.tsx
import { DiscordJSX } from '@nexcord/tsx';
import { TextInputStyle } from 'discord.js';
import { SupportAction } from '../common/actions/Support.action';

export function SupportModal() {
  return (
    <modal id="support_modal" title="Contact Support" action={SupportAction}>
      <label label="What do you need help with?" description="Pick the closest match">
        <stringSelect id="topic">
          <option label="Billing" value="billing" />
          <option label="Bug report" value="bug" />
          <option label="Something else" value="other" />
        </stringSelect>
      </label>
      <label label="Details">
        <input id="details" style={TextInputStyle.Paragraph} min={10} max={1000} />
      </label>
    </modal>
  );
}
// src/common/actions/Support.action.ts
import { Listener } from '@nexcord/core';
import type { ModalSubmitInteraction } from 'discord.js';

@Listener()
export class SupportAction {
  async execute(interaction: ModalSubmitInteraction, inputs: Record<string, any>): Promise<void> {
    const [topic] = inputs.topic as string[];
    const details = inputs.details as string;

    await interaction.reply({ content: `Thanks! Logged a **${topic}** request.`, ephemeral: true });
  }
}

Example: A Components V2 Layout

// src/components/status.components.tsx
import { DiscordJSX } from '@nexcord/tsx';
import { SeparatorSpacingSize } from 'discord.js';

export function StatusCard(props: { online: boolean; latency: number }) {
  return (
    <container accentColor={props.online ? 0x2ecc71 : 0xe74c3c}>
      <section>
        <text>**Bot Status**</text>
        <text>{props.online ? `🟢 Online — ${props.latency}ms` : '🔴 Offline'}</text>
        <thumbnail url="https://example.com/bot-avatar.png" />
      </section>
      <split divider spacing={SeparatorSpacingSize.Small} />
      <text>Data refreshes every 60 seconds.</text>
    </container>
  );
}

Important: Components V2 elements (<container>, <section>, <text>, <split>, <thumbnail>, <file>) require the message to be sent with Discord's IS_COMPONENTS_V2 flag — @nexcord/tsx builds the components, but you still send the message:

import { MessageFlags } from 'discord.js';
import { StatusCard } from '../components/status.components';

await interaction.reply({
  components: [StatusCard({ online: true, latency: 42 })],
  flags: MessageFlags.IsComponentsV2
});

Once a message is sent with this flag, its content and embeds fields stop working in favor of <text> and <container> — this is a Discord API rule, not a @nexcord/tsx limitation.

Custom / Function Components

Any function can be used as a JSX element. It receives its props plus a children array (flattened one level deep) as a single object argument, and can return one element or an array of them — exactly like composing components in React:

function Field({ name, value }: { name: string; value: string }) {
  return <field name={name}>{value}</field>;
}

<embed title="Profile" description="…">
  <Field name="Level" value="12" />
  <Field name="XP" value="4,210" />
</embed>

Notes & Current Limitations

  • Select menu listeners aren't de-duplicated yet. <button> and <modal> track their id internally and only ever register one interactionCreate listener per id, no matter how many times the element gets rebuilt. The five select-menu elements don't currently perform that check — reconstructing the same select (same id) repeatedly, e.g. inside a command that runs often, adds another listener each time. Where you can, build selects once and reuse the reference.
  • This is beta software, tracking @nexcord/core's beta status — expect the API to tighten up before 1.0.

License

Released under the MIT License.


Part of the Nexcord ecosystem — see @nexcord/core and @nexcord/config · Built by SignorMassimo