Components
Pre-built Discord UI components for your Convex + Next.js app. Install with one command, own the source.
Installation
Components are copied directly into your project — you own the code. Each component uses useQuery from Convex to fetch synced Discord data in real time.
Enable sync in your config and generate schema + mutations
npx calabasas generateAdd the components you need
npx calabasas add channel-select role-selectInstall the shadcn primitives they depend on
npx shadcn@latest add popover command buttonRun npx calabasas add without arguments for an interactive picker.
Available Components
Channel Select
calabasas add channel-selectSearchable combobox for picking a Discord channel. Displays channel type icons — hash for text, speaker for voice, megaphone for announcements — so users can visually distinguish channels at a glance.
Preview
Usage
import { ChannelSelect } from "@/components/calabasas/channel-select";
<ChannelSelect
guildDiscordId="123456789"
onValueChange={(id) => console.log(id)}
/>
{/* Only show text channels (0) and announcement channels (5) */}
<ChannelSelect
guildDiscordId="123456789"
channelTypes={[0, 5]}
onValueChange={(id) => console.log(id)}
/>Convex Query
import { query } from "../_generated/server";
import { v } from "convex/values";
export const listChannels = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
type: v.number(),
position: v.number(),
parentId: v.optional(v.string()),
})
),
handler: async (ctx, { guildDiscordId }) => {
const channels = await ctx.db
.query("calabasasChannels")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return channels.map((c) => ({
discordId: c.discordId,
name: c.name,
type: c.type,
position: c.position,
parentId: c.parentId,
}));
},
});Requires channels sync enabled in your calabasas.config.ts
Role Select
calabasas add role-selectSearchable combobox for picking a Discord role. Each role shows its color as a small dot, matching what users see in Discord. Roles without a custom color display the default gray.
Preview
Usage
import { RoleSelect } from "@/components/calabasas/role-select";
<RoleSelect
guildDiscordId="123456789"
onValueChange={(id) => console.log(id)}
/>Convex Query
import { query } from "../_generated/server";
import { v } from "convex/values";
export const listRoles = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
color: v.number(),
position: v.number(),
})
),
handler: async (ctx, { guildDiscordId }) => {
const roles = await ctx.db
.query("calabasasRoles")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return roles.map((r) => ({
discordId: r.discordId,
name: r.name,
color: r.color,
position: r.position,
}));
},
});Requires roles sync enabled in your calabasas.config.ts
Member Select
calabasas add member-selectSearchable combobox for picking a Discord server member. Shows avatar and display name. Falls back to a generic icon when no avatar is available. Bot accounts show a small badge.
Preview
Usage
import { MemberSelect } from "@/components/calabasas/member-select";
<MemberSelect
guildDiscordId="123456789"
onValueChange={(id) => console.log(id)}
/>Convex Query
import { query } from "../_generated/server";
import { v } from "convex/values";
export const listMembers = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordUserId: v.string(),
username: v.string(),
displayName: v.optional(v.string()),
avatar: v.optional(v.string()),
nick: v.optional(v.string()),
})
),
handler: async (ctx, { guildDiscordId }) => {
const members = await ctx.db
.query("calabasasMembers")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return members.map((m) => ({
discordUserId: m.discordUserId,
username: m.username,
displayName: m.displayName,
avatar: m.avatar,
nick: m.nick,
}));
},
});Requires members sync enabled in your calabasas.config.ts
Guild Select
calabasas add guild-selectSearchable combobox for picking a Discord server. Shows the guild icon when available, or a two-letter initials fallback. Unlike the other selects, this one doesn't take a guildDiscordId — it lists all synced guilds.
Preview
Usage
import { GuildSelect } from "@/components/calabasas/guild-select";
<GuildSelect
onValueChange={(id) => console.log(id)}
/>Convex Query
import { query } from "../_generated/server";
import { v } from "convex/values";
export const listGuilds = query({
args: {},
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
icon: v.optional(v.string()),
memberCount: v.optional(v.number()),
})
),
handler: async (ctx) => {
const guilds = await ctx.db
.query("calabasasGuilds")
.collect();
return guilds.map((g) => ({
discordId: g.discordId,
name: g.name,
icon: g.icon,
memberCount: g.memberCount,
}));
},
});Requires guilds sync enabled in your calabasas.config.ts
Role Badge
calabasas add role-badgeRenders Discord roles as colored pill badges — just like the role tags you see in Discord profiles. Pass a single role ID or an array; excess roles collapse into a '+N more' tooltip. Excludes @everyone automatically.
Preview
Usage
import { RoleBadge } from "@/components/calabasas/role-badge";
<RoleBadge
guildDiscordId="123456789"
roleIds={["role_id_1", "role_id_2"]}
/>
{/* Limit visible roles */}
<RoleBadge
guildDiscordId="123456789"
roleIds={member.roleIds}
maxDisplay={3}
size="sm"
/>Convex Query
export const listRolesForBadges = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
color: v.number(),
position: v.number(),
})
),
handler: async (ctx, { guildDiscordId }) => {
const roles = await ctx.db
.query("calabasasRoles")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return roles.map((r) => ({
discordId: r.discordId,
name: r.name,
color: r.color,
position: r.position,
}));
},
});Requires roles sync enabled in your calabasas.config.ts
Server Info
calabasas add server-infoDisplays a guild overview card with server icon (or initials fallback), name, member count, Nitro boost tier badge, and Discord feature tags like COMMUNITY, VERIFIED, and PARTNERED. Available in card and inline variants.
Preview
My Awesome Server
Usage
import { ServerInfo } from "@/components/calabasas/server-info";
<ServerInfo guildDiscordId="123456789" />
{/* Inline variant (no card wrapper) */}
<ServerInfo
guildDiscordId="123456789"
variant="inline"
/>
{/* Hide features and boost tier */}
<ServerInfo
guildDiscordId="123456789"
showFeatures={false}
showBoostTier={false}
/>Convex Query
export const getGuild = query({
args: { guildDiscordId: v.string() },
returns: v.union(
v.object({
discordId: v.string(),
name: v.string(),
icon: v.optional(v.string()),
memberCount: v.optional(v.number()),
premiumTier: v.number(),
features: v.array(v.string()),
}),
v.null()
),
handler: async (ctx, { guildDiscordId }) => {
const guild = await ctx.db
.query("calabasasGuilds")
.withIndex("by_discord_id", (q) =>
q.eq("discordId", guildDiscordId)
)
.first();
if (!guild) return null;
return {
discordId: guild.discordId,
name: guild.name,
icon: guild.icon,
memberCount: guild.memberCount,
premiumTier: guild.premiumTier,
features: guild.features,
};
},
});Requires guilds sync enabled in your calabasas.config.ts
Permission Viewer
calabasas add permission-viewerRead-only permission grid that parses Discord's permission bitfield into categorized sections (General, Text, Voice, Advanced). Green checkmarks for granted permissions, muted X for denied. Supports viewing by role ID or raw bitfield string.
Preview
General
Text
Usage
import { PermissionViewer } from "@/components/calabasas/permission-viewer";
{/* View permissions for a specific role */}
<PermissionViewer
guildDiscordId="123456789"
roleDiscordId="role_id"
/>
{/* View raw permission bitfield */}
<PermissionViewer permissions="1099511627775" />
{/* Compact badge mode */}
<PermissionViewer
guildDiscordId="123456789"
roleDiscordId="role_id"
compact
/>Convex Query
export const getRolePermissions = query({
args: {
guildDiscordId: v.string(),
roleDiscordId: v.string(),
},
returns: v.union(
v.object({
discordId: v.string(),
name: v.string(),
color: v.number(),
permissions: v.string(),
}),
v.null()
),
handler: async (ctx, { guildDiscordId, roleDiscordId }) => {
const roles = await ctx.db
.query("calabasasRoles")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
const role = roles.find(
(r) => r.discordId === roleDiscordId
);
if (!role) return null;
return {
discordId: role.discordId,
name: role.name,
color: role.color,
permissions: role.permissions,
};
},
});Requires roles sync enabled in your calabasas.config.ts
Member Card
calabasas add member-cardCompact profile card showing avatar, display name, username, BOT badge, colored role pills, and server join date. First component that fetches from both members and roles tables in a single query.
Preview
Roles
Joined
Jan 15, 2024
Usage
import { MemberCard } from "@/components/calabasas/member-card";
<MemberCard
guildDiscordId="123456789"
memberDiscordUserId="user_id"
/>
{/* Limit visible roles */}
<MemberCard
guildDiscordId="123456789"
memberDiscordUserId="user_id"
maxRoles={3}
showJoinDate={false}
/>Convex Query
export const getMemberWithRoles = query({
args: {
guildDiscordId: v.string(),
memberDiscordUserId: v.string(),
},
returns: v.union(
v.object({
member: v.object({
discordUserId: v.string(),
username: v.string(),
displayName: v.optional(v.string()),
avatar: v.optional(v.string()),
nick: v.optional(v.string()),
bot: v.optional(v.boolean()),
joinedAt: v.string(),
roleIds: v.array(v.string()),
}),
roles: v.array(
v.object({
discordId: v.string(),
name: v.string(),
color: v.number(),
position: v.number(),
})
),
}),
v.null()
),
handler: async (ctx, args) => {
const member = await ctx.db
.query("calabasasMembers")
.withIndex("by_user_guild", (q) =>
q.eq("discordUserId", args.memberDiscordUserId)
.eq("guildDiscordId", args.guildDiscordId)
)
.first();
if (!member) return null;
const allRoles = await ctx.db
.query("calabasasRoles")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", args.guildDiscordId)
)
.collect();
const memberRoles = allRoles.filter((r) =>
member.roles.includes(r.discordId)
);
return { member: { ... }, roles: memberRoles.map(...) };
},
});Requires members, roles sync enabled in your calabasas.config.ts
Channel Tree
calabasas add channel-treeHierarchical channel sidebar that groups channels by category with collapsible sections, type icons, and position sorting. Uncategorized channels appear at the top. Optional topic tooltips on hover.
Preview
Usage
import { ChannelTree } from "@/components/calabasas/channel-tree";
<ChannelTree
guildDiscordId="123456789"
onValueChange={(id) => console.log(id)}
/>
{/* Show only text channels, with topic tooltips */}
<ChannelTree
guildDiscordId="123456789"
channelTypes={[0, 5]}
showTopics
onValueChange={(id) => console.log(id)}
/>Convex Query
export const listChannelsTree = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
type: v.number(),
position: v.number(),
parentId: v.optional(v.string()),
topic: v.optional(v.string()),
})
),
handler: async (ctx, { guildDiscordId }) => {
const channels = await ctx.db
.query("calabasasChannels")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return channels.map((c) => ({
discordId: c.discordId,
name: c.name,
type: c.type,
position: c.position,
parentId: c.parentId,
topic: c.topic,
}));
},
});Requires channels sync enabled in your calabasas.config.ts
Member Roster
calabasas add member-rosterReplicates Discord's right sidebar member list. Members are grouped under their highest hoisted role with colored sticky headers and member counts. Members without hoisted roles appear in an 'Online' section.
Preview
Usage
import { MemberRoster } from "@/components/calabasas/member-roster";
<MemberRoster
guildDiscordId="123456789"
onValueChange={(id) => console.log(id)}
/>
{/* Limit members and hide bots */}
<MemberRoster
guildDiscordId="123456789"
maxMembers={50}
showBots={false}
/>Convex Query
export const listMembersWithHoistedRoles = query({
args: { guildDiscordId: v.string() },
returns: v.object({
members: v.array(
v.object({
discordUserId: v.string(),
username: v.string(),
displayName: v.optional(v.string()),
avatar: v.optional(v.string()),
nick: v.optional(v.string()),
bot: v.optional(v.boolean()),
roles: v.array(v.string()),
})
),
hoistedRoles: v.array(
v.object({
discordId: v.string(),
name: v.string(),
color: v.number(),
position: v.number(),
hoist: v.boolean(),
})
),
}),
handler: async (ctx, { guildDiscordId }) => {
const [members, roles] = await Promise.all([
ctx.db.query("calabasasMembers")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
).collect(),
ctx.db.query("calabasasRoles")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
).collect(),
]);
return {
members: members.map((m) => ({ ... })),
hoistedRoles: roles.filter((r) => r.hoist)
.map((r) => ({ ... })),
};
},
});Requires members, roles sync enabled in your calabasas.config.ts
Emoji Picker
calabasas add emoji-pickerDiscord-style emoji picker with category sidebar navigation, lazy-loaded custom Discord emojis (guild + application), bundled Unicode emojis, animated GIF hover previews, search filtering, and configurable single or multi-select mode.
Preview
Usage
import { EmojiPicker } from "@/components/calabasas/emoji-picker";
{/* Single select (default) */}
<EmojiPicker
guildDiscordId="123456789"
onSelect={(emoji) => console.log(emoji)}
/>
{/* Multi select with max 5 */}
<EmojiPicker
guildDiscordId="123456789"
mode="multi"
maxCount={5}
onChange={(emojis) => console.log(emojis)}
/>
{/* Default Unicode emojis only (no guild needed) */}
<EmojiPicker
source="default"
onSelect={(emoji) => console.log(emoji)}
/>Convex Query
export const listGuildEmojis = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
animated: v.boolean(),
available: v.boolean(),
})
),
handler: async (ctx, { guildDiscordId }) => {
const emojis = await ctx.db
.query("calabasasEmojis")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return emojis
.filter((e) => e.available)
.map((e) => ({
discordId: e.discordId,
name: e.name,
animated: e.animated,
available: e.available,
}));
},
});
export const listAppEmojis = query({
args: {},
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
animated: v.boolean(),
available: v.boolean(),
})
),
handler: async (ctx) => {
const emojis = await ctx.db
.query("calabasasAppEmojis")
.collect();
return emojis
.filter((e) => e.available)
.map((e) => ({
discordId: e.discordId,
name: e.name,
animated: e.animated,
available: e.available,
}));
},
});Requires emojis sync enabled in your calabasas.config.ts
Role Creator
calabasas add role-creatorDiscord-style role creation dialog with a 20-color preset palette (matching Discord's own role colors), custom hex input with native color picker, searchable member assignment with chip badges, and a visual role position editor with up/down controls.
Preview
Usage
import { RoleCreator } from "@/components/calabasas/role-creator";
{/* Basic usage with callback */}
<RoleCreator
guildDiscordId="123456789"
onCreate={(data) => {
console.log(data.name); // "Moderator"
console.log(data.color); // 3447003 (Discord int)
console.log(data.memberIds); // ["user1", "user2"]
console.log(data.position); // 3
}}
/>
{/* Pre-select members to assign the role to */}
<RoleCreator
guildDiscordId="123456789"
defaultMemberIds={["user_id_1", "user_id_2"]}
onCreate={handleCreate}
/>
{/* Controlled open state */}
<RoleCreator
guildDiscordId="123456789"
open={isOpen}
onOpenChange={setIsOpen}
onCreate={handleCreate}
/>
{/* Custom trigger */}
<RoleCreator
guildDiscordId="123456789"
trigger={<button>+ Add Role</button>}
onCreate={handleCreate}
/>Convex Query
export const listRolesForCreator = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordId: v.string(),
name: v.string(),
color: v.number(),
position: v.number(),
})
),
handler: async (ctx, { guildDiscordId }) => {
const roles = await ctx.db
.query("calabasasRoles")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return roles.map((r) => ({
discordId: r.discordId,
name: r.name,
color: r.color,
position: r.position,
}));
},
});
export const listMembersForCreator = query({
args: { guildDiscordId: v.string() },
returns: v.array(
v.object({
discordUserId: v.string(),
username: v.string(),
displayName: v.optional(v.string()),
avatar: v.optional(v.string()),
nick: v.optional(v.string()),
})
),
handler: async (ctx, { guildDiscordId }) => {
const members = await ctx.db
.query("calabasasMembers")
.withIndex("by_guild", (q) =>
q.eq("guildDiscordId", guildDiscordId)
)
.collect();
return members.map((m) => ({
discordUserId: m.discordUserId,
username: m.username,
displayName: m.displayName,
avatar: m.avatar,
nick: m.nick,
}));
},
});Requires roles + members sync enabled in your calabasas.config.ts
What Gets Installed
Each component writes two things into your project. You own both files and can customize them freely.
components/calabasas/React component using shadcn primitives (Popover + Command + Button) and Convex useQuery.
convex/calabasas/queries.tsConvex query functions. New queries are appended — existing ones are never overwritten.