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.

1

Enable sync in your config and generate schema + mutations

npx calabasas generate
2

Add the components you need

npx calabasas add channel-select role-select
3

Install the shadcn primitives they depend on

npx shadcn@latest add popover command button

Run npx calabasas add without arguments for an interactive picker.

Available Components

Channel Select

calabasas add channel-select

Searchable 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

general
Search channels...
general
announcements
voice-chat
dev-logs
random

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-select

Searchable 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

Admin
Search roles...
Admin
Moderator
Developer
Member

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-select

Searchable 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

DDarkViper
Search members...
DDarkViper
CoolUser99
PPixelQueen
ServerBotBOT

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-select

Searchable 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

MAMy Awesome Server
Search servers...
MAMy Awesome Server
TGTest Guild
DCDev Community

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-badge

Renders 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

AdminModeratorDeveloper+2 more

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-info

Displays 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

MA

My Awesome Server

1,234 membersLevel 2
CommunityDiscoverableWelcome Screen

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-viewer

Read-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

AdminAdministrator

General

Administrator
Manage Server
Manage Roles
Kick Members

Text

Send Messages
Embed Links
Manage Messages

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-card

Compact 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

DV
DarkViper
darkviper99

Roles

AdminDeveloper

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-tree

Hierarchical 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

TEXT CHANNELS
general
announcements
dev-logs
VOICE CHANNELS
voice-chat
music

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-roster

Replicates 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

Admin — 2
DV
DarkViper
PQ
PixelQueen
Developer — 1
CU
ServerBotBOT
Online — 1
CoolUser99

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-picker

Discord-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

🎉party popper
Search emojis...
😀
👋
🐶
🍎
🚗
Server Emojis
🟣
🔵
🟢
🟡
Smileys
😀
😂
🥰
😎

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-creator

Discord-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

Create Role
Create Role
Configure appearance, members, and position.
ModeratorPreview
Role Color
Default
#000000
Add Members (2)
AAliceBBob
Search by name or ID...
Position
Admin
ModeratorNEW
Member
@everyone

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.ts

Convex query functions. New queries are appended — existing ones are never overwritten.