🧩 kompozr is a composable UI layer for discord.js bots, providing ergonomic wrapper functions for building Discord UI components with less boilerplate and a focus on developer experience.
- About
- Installation
- Quick Start
- Components & Usage
- Full Example
- Full Example With Reactive Components
- API Reference
- TypeScript Support
- Contributing
- License
kompozr was created to improve the developer experience when building
Discord bots with discord.js.
The Discord.js builder API is powerful, but can be verbose and repetitive for
common UI patterns. kompozr introduces a philosophy of "less builders":
- Compose UIs with fewer lines of code
- Use simple, declarative wrappers instead of chaining builder methods
- Focus on what your UI should do, not how to wire up every builder
kompozr is open source and welcomes contributions! If you have ideas, improvements, or new components, feel free to open a PR or issue on GitHub.
- Developer Experience First: Less boilerplate, more readable code, and a declarative API.
- Composable: Easily combine components and layouts.
- All Discord UI Components: Buttons, select menus (all types), modals, inputs, galleries, files, and more.
- Layout Helpers: Rows, sections, containers, separators, and flexible layouts.
- Type-safe: Written in TypeScript with full type definitions.
- Less Builders Philosophy: No more endless
.addX()and.setY()chains—just describe your UI in objects and arrays. - Reactive Utilities: Advanced helpers for stateful, memoized, and reusable UI fragments.
- Open Source: Contributions and PRs are welcome!
npm install kompozrImport the main API object:
import { k } from "kompozr";Buttons are interactive components that users can click. With kompozr, you can
easily create all Discord button styles using a simple and consistent API. Each
button requires a cid (custom id) and a label or emoji. You can also add
both and set the button as disabled.
const button = k.button.primary({
cid: "my_button",
label: "Click me!",
emoji: "👋",
});Other button styles:
k.button.secondary– Gray button for secondary actions.k.button.success– Green button for positive actions.k.button.danger– Red button for destructive actions.k.button.link– Link button (useurlinstead ofcid).
const linkButton = k.button.link({
url: "https://github.com/silentadv/kompozr",
label: "GitHub",
});Select menus allow users to pick one or more options from a dropdown. kompozr
supports all Discord select menu types, including string, role, user, channel,
and mentionable selects. Each select requires a cid and an array of options.
Default Values:
For select menus that support default values (user, role, channel, mentionable), use thedefaultValuesproperty and the helpers:
k.selectValue.user(id)k.selectValue.role(id)k.selectValue.channel(id)
const selectMenu = k.select.string({
cid: "my_select",
options: [
{ value: "1", label: "Option 1" },
{ value: "2", label: "Option 2" },
],
placeholder: "Choose an option",
});Other select types:
k.select.role– Select one or more roles.k.select.user– Select one or more users.k.select.channel– Select one or more channels.k.select.mentionable– Select users or roles.
const channelSelect = k.select.channel({
cid: "channel_select",
placeholder: "Pick a channel",
channelTypes: [ChannelType.GuildText, ChannelType.GuildVoice],
});const userSelect = k.select.user({
cid: "user_select",
placeholder: "Pick a user",
defaultValues: [
k.selectValue.user("user-id-1"),
k.selectValue.user("user-id-2"),
],
});Modals are pop-up forms used to collect user input. In kompozr, you can build modals with short or paragraph text inputs.
Each input requires a cid and a label. Inputs now automatically return a
label component that wraps the input as its component. This also makes the
label and description properties available directly on the input functions:
const modal = k.modal({
cid: "feedback_modal",
title: "Feedback",
fields: [
k.input.short({
cid: "username",
label: "Your Name",
description: "So we know who you are",
required: true,
}),
k.input.paragraph({
cid: "feedback",
label: "Your Feedback",
description: "Tell us what you think!",
required: true,
}),
],
});In addition to inputs, you can also create labels manually using k.label. This
allows you to wrap select menus and text displays — in a label with its own
label and description:
const labeledSelect = k.label({
label: "Pick an option",
description: "Choose wisely",
component: k.select.string({
cid: "my_select",
options: [
{ value: "1", label: "Option 1" },
{ value: "2", label: "Option 2" },
],
placeholder: "Choose an option",
}),
});
const modal = k.modal({
cid: "option_modal",
title: "Option Form",
fields: [k.text("Hello, World", "Display Text"), "## Form:", labeledSelect],
});kompozr provides helpers to organize your UI components into rows, sections, containers, and layouts.
Use row only to group buttons. Discord allows only one select menu per row,
and kompozr's select wrappers already return a row containing the select menu.
const row = k.row(
k.button.success({ cid: "ok", label: "OK" }),
k.button.danger({ cid: "cancel", label: "Cancel" })
);Sections let you combine text and an accessory (like a button or select) in a
single block. Use section to align text (max 3 text display) side by side with
an accessory, such as a button or a thumbnail. The main content can be a plain
string or a component created with k.text. This makes it easy to display a
message with an interactive element or image next to it.
const section = k.section({
components: ["Welcome to the server!", k.text("Enjoy your stay!")],
accessory: k.button.primary({ cid: "welcome", label: "Say Hi!" }),
});Separators visually divide content. Choose from different sizes and visibility.
k.separator.small; // small spacing
k.separator.large; // large spacing
k.separator.smallHidden; // small spacing without divider (only space)
k.separator.largeHidden; // large spacing without divider (only space)Containers are an layout block for messages, is a component composer, allowing you to group sections, rows, separators, and other components. You can also set a color for the container.
const container = k.container({
components: [
section,
k.row(
k.button.success({ cid: "ok", label: "OK" }),
k.button.danger({ cid: "cancel", label: "Cancel" })
),
k.separator.small,
"good text here.",
k.text("line 1", "line 2"),
],
color: "#FF0000", // or 0xff #FFF
});The layout utility is a simple helper that lets you combine any
components—including plain strings—into a single array. This means you can use
strings directly in your layouts without always needing to wrap them with
k.text or a text display builder. It's mainly for convenience and advanced
custom layouts, and does not support color like container.
const layout = k.layout(
k.text("Header"),
k.row(
k.button.primary({ cid: "a", label: "A" }),
k.button.secondary({ cid: "b", label: "B" })
),
k.separator.small,
"Hello World!"
);kompozr also provides helpers for displaying text, images, files, and media galleries.
Display plain or formatted text in your UI.
const text = k.text("Hello, world!");
const multiLineText = k.text("Line1", "Line2", "Line3");Add a thumbnail image with an optional description.
const thumbnail = k.thumbnail({
url: "https://example.com/thumb.png",
description: "A thumbnail",
});Attach a file to your message, with optional spoiler support.
const file = k.file({
url: "https://example.com/file.pdf",
spoiler: true,
});Display a gallery of images or media files. Each item can have a description and be marked as a spoiler.
const gallery = k.gallery(
{ url: "https://example.com/image1.png", description: "First image" },
{ url: "https://example.com/image2.png", spoiler: true }
);kompozr also provides a set of reactive utilities for advanced UI composition and state management. These are useful for building dynamic, stateful, or memoized UI fragments in your Discord bot.
- When you want to reuse UI fragments with different props (like React fragments).
- When you need to memoize expensive UI computations and only update when dependencies change.
- When you want to manage local state for a UI component or section.
Creates a reusable UI fragment (like a functional component).
Use when you want to generate repeated or parameterized UI blocks.
Example:
interface User {
id: string;
username: string;
}
// type anotation only is necessary in typescript projects.
// In javascript projects just call k.fragment(...)
const UserSection = k.fragment<User>((user) =>
k.section({
components: [`User: ${user.username}`],
accessory: k.button.primary({ cid: `user_${user.id}`, label: "Select" }),
})
);
// Usage:
const users = [
{ id: "1", username: "Alice" },
{ id: "2", username: "Bob" },
];
const userSections = UserSection(users); // returns an array of sectionsUse case:
Reusable UI blocks for lists, cards, or repeated sections.
Memoizes a UI fragment, only recomputing when dependencies change.
Use when you have expensive UI generation logic and want to avoid unnecessary
recomputation.
Example:
interface Props {
value: number;
}
// type anotation only is necessary in typescript projects.
// In javascript projects just use (props) => ... and (props) => [...]
const ExpensiveSection = k.memo(
(props: Props) =>
k.section({
components: [`Value: ${props.value}`],
accessory: k.button.primary({ cid: "btn", label: "Go" }),
}),
(props: Props) => [props.value] // dependencies
);
// Usage:
const section = ExpensiveSection({ value: 42 }); // build
ExpensiveSection({ value: 42 }); // cached
ExpensiveSection({ value: 44 }); // rebuild because dependencies are changed
ExpensiveSection({ value: 44 }); // cached
ExpensiveSection({ value: 42 }); // rebuild because dependencies are changedUse case:
Performance optimization for dynamic UIs that depend on changing props.
Creates a stateful UI component with local state and an update method.
Use when you want to encapsulate state and rendering logic together.
Example:
const counter = k.stateful({ count: 0 }, (state) =>
k.section({
components: [`Count: ${state.count}`],
accessory: k.button.primary({ cid: "inc", label: "Increment" }),
})
);
// Usage:
counter.render(); // renders with current state
counter.update({ count: counter.state.count + 1 }); // update stateUse case:
Local state management for interactive or dynamic UI sections.
import { k } from "kompozr";
const button = k.button.success({
cid: "ok_btn",
label: "OK",
});
const gallery = k.gallery({
url: "https://example.com/cat.png",
description: "A cute cat",
});
const message = k.container({
components: [
k.section({
components: ["Check out this gallery!"],
acessory: button,
}),
gallery,
k.separator.small,
k.text("Thanks for using kompozr!"),
],
color: "Green",
});
// Send `message` as your bot's responseconst UserCard = k.fragment<User>((user) =>
k.section({
components: [`👤 ${user.name}`],
accessory: k.button.primary({ cid: `select_${user.id}`, label: "Select" }),
})
);
const userList: User[] = [
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
];
const message = k.container({
components: [
...UserCard(userList), // list of sections
k.separator.small,
k.text("Select a user!"),
],
});All builder functions return Discord.js builder instances, ready to be used in your bot's responses.
- Buttons:
k.button.primary,k.button.secondary,k.button.success,k.button.danger,k.button.link - Select Menus:
k.select.string,k.select.role,k.select.user,k.select.channel,k.select.mentionable - Populated Select Menus Values:
k.selectValue.user,k.selectValue.role,k.selectValue.channel - Modals & Inputs:
k.modal,k.input.short,k.input.paragraph - Layout:
k.row,k.section,k.container,k.separator,k.layout - Content:
k.text,k.thumbnail,k.gallery,k.file - Reactive:
k.memo,k.stateful,k.fragment
kompozr is written in TypeScript and ships with full type definitions.
kompozr is open source and contributions are welcome!
If you have suggestions, bug reports, or want to add new features/components,
please open an issue or PR on GitHub.
MIT
Made with ❤️ by primepvi
