Skip to content

Buttons

Overview

components.Button has six current styles in this repository: primary, secondary, success, danger, link, and premium. Application-owned buttons use a CustomID and are routed by bot.Router; link buttons use a URL and navigate without sending a component interaction.

  1. Build a custom-ID button with SetCustomID, SetLabel, and SetStyle.
  2. Put it in an action row.
  3. Register the exact ID with router.Button.
  4. Acknowledge a click with UpdateContent, Update, or DeferUpdate.
  5. Build link buttons with SetURL and ButtonStyleLink; do not register a route for them.

Complete Runnable Example

Copy to examples/buttons/main.go, set DISCORD_TOKEN, and run it. Invoke /buttons, then try both controls.

go
package main

import (
	"log"
	"os"

	"github.com/discord-go/discord.go/bot"
	"github.com/discord-go/discord.go/components"
	"github.com/discord-go/discord.go/intents"
	"github.com/discord-go/discord.go/interactions"
)

func main() {
	token := os.Getenv("DISCORD_TOKEN")
	if token == "" {
		log.Fatal("DISCORD_TOKEN is required")
	}

	router := bot.NewRouter()
	router.Command("buttons", "Show button types", func(ctx *bot.InteractionContext) {
		custom := components.NewButtonBuilder().
			SetCustomID("buttons:done").
			SetLabel("Complete").
			SetStyle(components.ButtonStyleSuccess).
			Build()
		link := components.NewButtonBuilder().
			SetLabel("Discord developers").
			SetURL("https://discord.com/developers/docs").
			SetStyle(components.ButtonStyleLink).
			Build()
		row := components.NewActionRowBuilder().AddComponents(custom, link).Build()
		if err := ctx.ReplyComplex(&interactions.InteractionCallbackData{
			Content:    "The first button is routed by the bot. The second opens a URL.",
			Components: []components.Component{row},
		}); err != nil {
			log.Printf("button response: %v", err)
		}
	})

	router.Button("buttons:done", func(ctx *bot.InteractionContext) {
		if err := ctx.UpdateContent("Completed. The link button never reached the bot."); err != nil {
			log.Printf("button update: %v", err)
		}
	})

	b := bot.New(token, bot.WithIntents(intents.Guilds), bot.WithRouter(router))
	if err := b.Run(); err != nil {
		log.Fatal(err)
	}
}

Explanation

The string passed to SetCustomID must equal the string passed to router.Button. A custom ID is visible to clients and may be replayed, so it is a routing hint rather than a security boundary. If a button belongs to a specific user or resource, check that relationship inside the handler.

For IDs such as ticket:close:123, use router.ButtonPrefix("ticket:close:", handler), then read the suffix with ctx.Suffix() (which returns "123"), and verify that the actor is allowed to close the ticket before performing the operation.

Slow Handlers

The component response deadline is short. Defer before slow work:

go
router.Button("refresh", func(ctx *bot.InteractionContext) {
	if err := ctx.DeferUpdate(); err != nil {
		return
	}
	// Perform bounded work, then edit the original interaction response.
	if _, err := ctx.EditReply("Refreshed"); err != nil {
		log.Printf("edit button response: %v", err)
	}
})

UpdateContent is the simplest immediate acknowledgement. Update is useful when the new content, embeds, or component state must be sent together.

Common Mistakes

  • Calling Reply from a button handler when the source message should change.
  • Sleeping or doing REST work before DeferUpdate.
  • Assigning both CustomID and URL; link buttons should be navigation only.
  • Assuming disabled buttons enforce authorization.
  • Forgetting to return/log an error from the acknowledgement.

Expected Result

/buttons renders one application button and one link button. Clicking Complete updates the original message. Clicking the link opens the Discord developer documentation and does not invoke a router route.

Released under the Apache License 2.0.