Telebot’s keyboard API is built around the ReplyMarkup struct. A single markup instance can carry either a reply keyboard (shown below the text input) or an inline keyboard (embedded inside a message).
ReplyMarkup
type ReplyMarkup struct {
InlineKeyboard [][] InlineButton
ReplyKeyboard [][] ReplyButton
ForceReply bool
ResizeKeyboard bool
OneTimeKeyboard bool
RemoveKeyboard bool
Selective bool
Placeholder string
IsPersistent bool
}
Create a new instance with b.NewMarkup() or directly with a struct literal:
var menu = & tele . ReplyMarkup { ResizeKeyboard : true }
// — or —
menu := b . NewMarkup ()
Building reply keyboards
Reply keyboards appear as a persistent button panel below the input field. Use Btn constructors on the markup, then assemble rows and call .Reply().
var menu = & tele . ReplyMarkup { ResizeKeyboard : true }
btnContact := menu . Contact ( "Share contact" )
btnLocation := menu . Location ( "Share location" )
btnText := menu . Text ( "Just a label" )
btnPoll := menu . Poll ( "Create poll" , tele . PollQuiz )
menu . Reply (
menu . Row ( btnContact , btnLocation ),
menu . Row ( btnText ),
menu . Row ( btnPoll ),
)
b . Handle ( "/menu" , func ( c tele . Context ) error {
return c . Send ( "Choose:" , menu )
})
.Text(text) A plain-text button. Sends the label text as a message when tapped.
.Contact(text) Requests the user’s phone number.
.Location(text) Requests the user’s current location.
.Poll(text, pollType) Lets the user create and send a poll. Pass tele.PollQuiz or tele.PollRegular.
Removing the keyboard
Send RemoveKeyboard: true to dismiss the reply keyboard for the user:
b . Handle ( "/done" , func ( c tele . Context ) error {
return c . Send ( "Keyboard removed." , & tele . ReplyMarkup { RemoveKeyboard : true })
})
Or use the shortcut option flag:
return c . Send ( "Done!" , tele . RemoveKeyboard )
Building inline keyboards
Inline keyboards are attached to individual messages. Use .Data(), .URL(), .Query(), .QueryChat(), or .Login() constructors, then call .Inline().
var inlineMenu = & tele . ReplyMarkup {}
btnApprove := inlineMenu . Data ( "Approve" , "approve" , userID )
btnDeny := inlineMenu . Data ( "Deny" , "deny" , userID )
btnVisit := inlineMenu . URL ( "Visit site" , "https://example.com" )
btnSearch := inlineMenu . Query ( "Search" , "default query" )
inlineMenu . Inline (
inlineMenu . Row ( btnApprove , btnDeny ),
inlineMenu . Row ( btnVisit ),
inlineMenu . Row ( btnSearch ),
)
.Data(text, unique, data...) Fires a callback. unique is the handler endpoint; optional data strings are joined by | and available as c.Args().
.URL(text, url) Opens a URL in the browser when tapped.
.Query(text, query) Switches to inline mode with a pre-filled query in any chat.
.QueryChat(text, query) Switches to inline mode in the current chat.
.Login(text, login) Opens Telegram Login Widget flow. Accepts a *tele.Login struct.
.WebApp(text, app) Launches a Mini App. Accepts a *tele.WebApp.
The unique parameter doubles as the callback endpoint registered with b.Handle():
var selector = & tele . ReplyMarkup {}
btnYes := selector . Data ( "Yes" , "confirm_yes" , "some_data" )
btnNo := selector . Data ( "No" , "confirm_no" )
selector . Inline ( selector . Row ( btnYes , btnNo ))
// Register handlers using the button itself as the endpoint
b . Handle ( & btnYes , func ( c tele . Context ) error {
args := c . Args () // ["some_data"]
return c . Respond ( & tele . CallbackResponse { Text : "Confirmed!" })
})
b . Handle ( & btnNo , func ( c tele . Context ) error {
return c . Respond ( & tele . CallbackResponse { Text : "Cancelled." })
})
Always c.Respond() in callback handlers — Telegram shows a loading spinner until a response is sent.
Use menu.Row(btns...) for explicit row composition, or menu.Split(n, btns) to automatically chunk a slice into rows of n buttons:
btns := [] tele . Btn {
menu . Data ( "A" , "a" ), menu . Data ( "B" , "b" ),
menu . Data ( "C" , "c" ), menu . Data ( "D" , "d" ),
menu . Data ( "E" , "e" ), menu . Data ( "F" , "f" ),
}
// Creates [[A,B,C],[D,E,F]]
menu . Inline ( menu . Split ( 3 , btns ) ... )
The Login button opens the Telegram Login Widget flow and can forward the user back to your site:
loginBtn := menu . Login ( "Log in with Telegram" , & tele . Login {
URL : "https://example.com/auth" ,
Text : "Login to Example" ,
Username : "mybot" ,
WriteAccess : true ,
})
Full inline keyboard example
package main
import (
" log "
" time "
tele " gopkg.in/telebot.v4 "
)
func main () {
b , _ := tele . NewBot ( tele . Settings {
Token : "TOKEN" ,
Poller : & tele . LongPoller { Timeout : 10 * time . Second },
})
var menu = & tele . ReplyMarkup {}
btnLike := menu . Data ( "Like" , "like" )
btnDislike := menu . Data ( "Dislike" , "dislike" )
menu . Inline ( menu . Row ( btnLike , btnDislike ))
b . Handle ( "/post" , func ( c tele . Context ) error {
return c . Send ( "How do you like this post?" , menu )
})
b . Handle ( & btnLike , func ( c tele . Context ) error {
return c . Respond ( & tele . CallbackResponse { Text : "Thanks!" })
})
b . Handle ( & btnDislike , func ( c tele . Context ) error {
return c . Respond ( & tele . CallbackResponse { Text : "Sorry to hear that." })
})
log . Fatal ( b . Start ())
}