NapNodes

← Discord bot guides

Discord bots · updated 24 October 2026

Discord Role Bot: Buttons So Everyone Picks Their Own Roles (Python and JavaScript)

A role bot posts a message with buttons and everyone on your Discord server clicks the ones they want: the game they play, whether they want event pings, their region… Here it is in full in discord.py and discord.js, about 70 lines: a /role-panel command to post the message and the buttons that give and take each role. No database and none of the hassle of reaction roles.

Buttons, not reactions

For years “reaction roles” were the norm: you react with an emoji and the bot gives you the role. They work, but they have downsides: the bot needs the reactions intent, it has to store which emoji goes with which role on which message (and remember it after a restart), and whoever reacts doesn't know whether it worked until they check their roles.

Buttons solve all three. Each button carries its role's id inside it, so the bot stores nothing: when someone clicks, Discord sends it the clicked button and the bot knows which role to give, even if the message was posted months ago and the bot has restarted a thousand times. And the reply (“You now have @Minecraft”) is shown only to whoever clicked, without cluttering the channel.

If your Discord server is a community server, Discord also has its own “Onboarding”, with questions on joining that give roles, no bot needed. The bot works on every server, lets people change roles at any time from a channel and lets you have several panels (games, pings, colors) wherever you like.

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

1. Create the bot on Discord

  1. Create the application and get the token

    At discord.com/developers/applications, “New Application”; in the “Bot” tab, “Reset Token” and copy the token. It's the bot's password: don't share it or push it to GitHub. If it's your first bot, the step-by-step process is in your first bot in Python.

  2. Invite it with just the permissions it needs

    In “OAuth2” → “URL Generator”, tick “bot” and “applications.commands”, and in the permissions “Send Messages”, “Embed Links” and “Manage Roles”. Open the link it gives you and pick your server. No special intent is needed (the “Privileged Gateway Intents” stay off).

  3. Put its role above the ones it will give

    In “Server Settings” → “Roles”, drag the bot's role above the roles people will be able to pick. A bot can't give a role at its own level or above, even with “Manage Roles”. Other bots' roles and @everyone can never be given.

2. The code in Python (discord.py)

Three files in one folder. The token goes in .env, without quotes. You need Python 3.10 or newer.

requirements.txt
discord.py
python-dotenv
.env
DISCORD_TOKEN=paste-your-token-here
app.py
import os

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()  # reads the token from the .env file

# Only the default intents are needed: buttons arrive as interactions.
client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)


@tree.command(name="role-panel", description="Posts a message with buttons to pick roles")
@app_commands.guild_only()
@app_commands.default_permissions(manage_roles=True)  # only visible to whoever manages roles
@app_commands.describe(
    title="What goes above the buttons",
    role1="A role people can pick",
    role2="Another role (optional)",
    role3="Another role (optional)",
    role4="Another role (optional)",
    role5="Another role (optional)",
)
async def role_panel(
    interaction: discord.Interaction,
    title: str,
    role1: discord.Role,
    role2: discord.Role | None = None,
    role3: discord.Role | None = None,
    role4: discord.Role | None = None,
    role5: discord.Role | None = None,
):
    roles = [r for r in (role1, role2, role3, role4, role5) if r]
    mine = interaction.guild.me.top_role
    # A bot can't give a role equal to or higher than its own, nor other bots' roles or @everyone.
    impossible = [r.name for r in roles if r >= mine or r.managed or r.is_default()]
    if impossible:
        await interaction.response.send_message(
            f"I can't give {', '.join(impossible)}: move my role above them in “Server Settings” → “Roles”.",
            ephemeral=True,
        )
        return
    view = discord.ui.View(timeout=None)
    for role in roles:
        # The role id goes in the button itself: no database needed.
        view.add_item(discord.ui.Button(label=role.name, custom_id=f"rid:{role.id}"))
    card = discord.Embed(title=title, description="Click a button to add or remove the role.", color=0x5B4AE8)
    await interaction.channel.send(embed=card, view=view)
    await interaction.response.send_message("Panel posted.", ephemeral=True)


@client.event
async def on_interaction(interaction: discord.Interaction):
    # Every button click arrives here, also on panels posted before the bot restarted.
    if interaction.type is not discord.InteractionType.component:
        return
    custom_id = interaction.data.get("custom_id", "")
    if not custom_id.startswith("rid:") or interaction.guild is None:
        return
    role = interaction.guild.get_role(int(custom_id[4:]))
    if role is None:
        await interaction.response.send_message("That role no longer exists.", ephemeral=True)
        return
    try:
        if role in interaction.user.roles:
            await interaction.user.remove_roles(role, reason="Role panel")
            text = f"I've removed {role.mention}."
        else:
            await interaction.user.add_roles(role, reason="Role panel")
            text = f"You now have {role.mention}."
    except discord.Forbidden:
        text = "I can't give you that role: tell whoever manages the server (my role has to be above it)."
    await interaction.response.send_message(text, ephemeral=True)


@client.event
async def on_ready():
    await tree.sync()  # publishes /role-panel
    print(f"Logged in as {client.user}")


client.run(os.environ["DISCORD_TOKEN"])

3. The same bot in JavaScript (discord.js)

With Node.js 18 or newer. Replies that only the clicker sees are marked with MessageFlags.Ephemeral, which is the current way in discord.js 14 (the old one, ephemeral: true, warns that it's going away).

package.json
{
  "name": "role-bot",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "discord.js": "^14.17.0",
    "dotenv": "^16.4.0"
  }
}
.env
DISCORD_TOKEN=paste-your-token-here
index.js
require("dotenv").config(); // reads the token from the .env file
const {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  Client,
  EmbedBuilder,
  Events,
  GatewayIntentBits,
  InteractionContextType,
  MessageFlags,
  PermissionFlagsBits,
  SlashCommandBuilder,
} = require("discord.js");

// Only Guilds is needed: buttons arrive as interactions.
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const command = new SlashCommandBuilder()
  .setName("role-panel")
  .setDescription("Posts a message with buttons to pick roles")
  .setContexts(InteractionContextType.Guild)
  .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles) // only visible to whoever manages roles
  .addStringOption((o) => o.setName("title").setDescription("What goes above the buttons").setRequired(true))
  .addRoleOption((o) => o.setName("role1").setDescription("Role").setRequired(true));
for (let n = 2; n <= 5; n++) command.addRoleOption((o) => o.setName(`role${n}`).setDescription("Another role (optional)"));

const privately = (content) => ({ content, flags: MessageFlags.Ephemeral });

client.on(Events.InteractionCreate, async (i) => {
  if (!i.inCachedGuild()) return;

  if (i.isChatInputCommand() && i.commandName === "role-panel") {
    const roles = [1, 2, 3, 4, 5].map((n) => i.options.getRole(`role${n}`)).filter(Boolean);
    const mine = i.guild.members.me.roles.highest;
    // A bot can't give a role equal to or higher than its own, nor other bots' roles or @everyone.
    const impossible = roles.filter((r) => r.managed || r.id === i.guildId || r.comparePositionTo(mine) >= 0);
    if (impossible.length) {
      const names = impossible.map((r) => r.name).join(", ");
      return i.reply(privately(`I can't give ${names}: move my role above them in “Server Settings” → “Roles”.`));
    }
    // The role id goes in the button itself: no database needed.
    const row = new ActionRowBuilder().addComponents(
      roles.map((r) => new ButtonBuilder().setCustomId(`rid:${r.id}`).setLabel(r.name).setStyle(ButtonStyle.Secondary)),
    );
    const card = new EmbedBuilder()
      .setColor(0x5b4ae8)
      .setTitle(i.options.getString("title"))
      .setDescription("Click a button to add or remove the role.");
    await i.channel.send({ embeds: [card], components: [row] });
    return i.reply(privately("Panel posted."));
  }

  // Every button click arrives here, also on panels posted before the bot restarted.
  if (i.isButton() && i.customId.startsWith("rid:")) {
    const role = i.guild.roles.cache.get(i.customId.slice(4));
    if (!role) return i.reply(privately("That role no longer exists."));
    let text;
    try {
      if (i.member.roles.cache.has(role.id)) {
        await i.member.roles.remove(role, "Role panel");
        text = `I've removed ${role}.`;
      } else {
        await i.member.roles.add(role, "Role panel");
        text = `You now have ${role}.`;
      }
    } catch {
      text = "I can't give you that role: tell whoever manages the server (my role has to be above it).";
    }
    await i.reply(privately(text));
  }
});

client.once(Events.ClientReady, async (c) => {
  await c.application.commands.set([command.toJSON()]); // publishes /role-panel
  console.log(`Logged in as ${c.user.tag}`);
});

client.login(process.env.DISCORD_TOKEN);

4. Post your first panel

/role-panel is only visible to people with the manage roles permission. If you want others to use it, you change that without touching the code in “Server Settings” → “Integrations” → your bot.

  1. Start the bot

    On your computer: “pip install -r requirements.txt” and “python app.py” (or “npm install” and “node index.js”). Wait until it says “Logged in as…”. The command can take a few minutes to show up the first time.

  2. Create the roles

    In “Server Settings” → “Roles”, create the ones people will be able to pick (for example Minecraft, Valheim and Event pings) and keep the bot's role above all of them.

  3. Type /role-panel in the channel

    Give it a title (“What do you play?”) and pick from one to five roles. The bot posts the message with one button per role and replies “Panel posted.” only to you. If any role is above its own, it tells you and posts nothing.

  4. Try it

    Click a button: it gives you the role and tells you. Click it again and it removes it. For more than five roles, post another panel below (for example one for games and another for pings).

If something isn't working

“I can't give you that role”: the bot's role has ended up below it (for example, someone moved the roles after the panel was posted) or “Manage Roles” was taken away. Move it up and click again; you don't need to post the panel again.

Clicking says “This interaction failed”: the bot is offline. The buttons stay there even when the bot isn't running, but nobody answers. As soon as it's back online, the old panels work by themselves.

“That role no longer exists”: the role was deleted. Delete that panel and post a new one with the current roles. If you only rename the role, the button keeps working, but shows the old name until you post another panel.

A pings role is useful for mentioning in announcements: it only reaches whoever picked it. In “Server Settings” → “Roles” → that role you decide whether anyone can mention it or only whoever manages the server. And for a game role to unlock channels, give it permission to view those channels in its settings.

5. Keep it online all the time (for free)

Buttons have the same problem as any bot: if it lives on your computer, when you switch it off the panels stay in the channel but clicking says the interaction failed, and whoever joins your server at night is left without their roles.

On NapNodes: create the free bot (it starts on Node.js; if yours is the Python one, in “Settings” → “Language”, “Switch to Python”), upload the three files in “Files” and click “Start”. The libraries in requirements.txt or package.json install themselves at startup, and app.py or index.js is the startup file that comes set. Since the bot stores nothing, you can move it, reinstall it or switch it from Python to JavaScript and the posted panels keep working.

The free bot has 256 MB, plenty for a role bot, and you have to renew it with one click every week: if you forget, it goes offline until you start it (and after 14 days without renewing it's deleted, with a warning beforehand). If you'd rather not have to remember, the Mini plan (512 MB) stays online with nothing to renew (Discord bot pricing). The whole setup is in the Discord bot hosting guide.

Ideas to extend it

Exclusive roles (pick only one, like colors or regions): when giving one, remove the others from the same panel, which are on the message's other buttons. A dropdown menu instead of buttons, for panels of up to 25 roles in a single message. Emojis on the buttons (setEmoji in discord.js, emoji= in discord.py). Or a role that can only be picked after some time on the server, by checking the member's join date before giving it.

Frequently asked questions

How do I make a self-role bot for Discord?

With a command that posts a message with buttons, one per role, and an event that receives each click: if the person already has the role, it removes it; if not, it gives it. The full code, in Python and JavaScript, is in this guide.

Are reaction roles or buttons better?

Buttons. The bot doesn't need to store which emoji goes with which role or the reactions intent, it works the same after a restart and it privately tells whoever clicked whether it gave or removed the role.

Why doesn't my bot give roles?

It's almost always the hierarchy: the bot's role has to be above the role it gives, and the bot needs the “Manage Roles” permission. Other bots' roles and @everyone can't be given.

Do the buttons keep working if I restart the bot?

Yes, with this guide's code: each button carries its role's id and the bot handles any click, whether from a new panel or one posted before the restart. The bot just has to be online.

How many roles fit in a panel?

Five with this guide's code, which uses one row of buttons. For more, post several panels, or swap the buttons for a dropdown menu, which allows up to 25 options.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Free tool: Discord colored text generator. Write on Discord in red, green or yellow, with backgrounds, bold and underline: the ansi block ready to paste, and the line for your bot.

Keep reading

All Discord bot guides, sorted →