The Telegram Bot Framework for Node.js and Deno with middleware-based architecture and full TypeScript support
Build custom reply keyboards and inline keyboards with fluent, chainable APIs supporting all Telegram keyboard button types.
Custom keyboards that replace the user's keyboard with bot-defined buttons.
/**
* Builder for custom reply keyboards that appear in place of
* the user's system keyboard
*/
class Keyboard {
/**
* Create keyboard builder
* @param keyboard - Optional initial keyboard layout
*/
constructor(keyboard?: KeyboardButton[][]);
/**
* 2D array of keyboard buttons
*/
keyboard: KeyboardButton[][];
/**
* Keep keyboard visible after button press
*/
is_persistent?: boolean;
/**
* Show keyboard only to specific users
*/
selective?: boolean;
/**
* Hide keyboard after single use
*/
one_time_keyboard?: boolean;
/**
* Automatically resize keyboard
*/
resize_keyboard?: boolean;
/**
* Placeholder text in input field
*/
input_field_placeholder?: string;
/**
* Add buttons to current row
* @param buttons - Buttons to add
* @returns This keyboard for chaining
*/
add(...buttons: KeyboardButton[]): this;
/**
* Start new row with buttons
* @param buttons - Buttons for new row
* @returns This keyboard for chaining
*/
row(...buttons: KeyboardButton[]): this;
/**
* Add text button to current row
* @param text - Button text
* @returns This keyboard for chaining
*/
text(text: string): this;
/**
* Add request users button
* @param text - Button text
* @param requestId - Request identifier
* @param options - Additional options
* @returns This keyboard for chaining
*/
requestUsers(
text: string,
requestId: number,
options?: Omit<KeyboardButton.RequestUsersButton, "text" | "request_users">
): this;
/**
* Add request chat button
* @param text - Button text
* @param requestId - Request identifier
* @param options - Additional options
* @returns This keyboard for chaining
*/
requestChat(
text: string,
requestId: number,
options?: Omit<KeyboardButton.RequestChatButton, "text" | "request_chat">
): this;
/**
* Add request contact button
* @param text - Button text
* @returns This keyboard for chaining
*/
requestContact(text: string): this;
/**
* Add request location button
* @param text - Button text
* @returns This keyboard for chaining
*/
requestLocation(text: string): this;
/**
* Add request poll button
* @param text - Button text
* @param type - Poll type (optional)
* @returns This keyboard for chaining
*/
requestPoll(text: string, type?: PollType): this;
/**
* Add web app button
* @param text - Button text
* @param url - Web app URL
* @returns This keyboard for chaining
*/
webApp(text: string, url: string): this;
/**
* Set persistent flag
* @param isEnabled - Enable persistence (default: true)
* @returns This keyboard for chaining
*/
persistent(isEnabled?: boolean): this;
/**
* Set selective flag
* @param isEnabled - Enable selective (default: true)
* @returns This keyboard for chaining
*/
selected(isEnabled?: boolean): this;
/**
* Set one-time flag
* @param isEnabled - Enable one-time (default: true)
* @returns This keyboard for chaining
*/
oneTime(isEnabled?: boolean): this;
/**
* Set resize flag
* @param isEnabled - Enable resize (default: true)
* @returns This keyboard for chaining
*/
resized(isEnabled?: boolean): this;
/**
* Set input field placeholder
* @param value - Placeholder text
* @returns This keyboard for chaining
*/
placeholder(value: string): this;
/**
* Transpose keyboard (swap rows and columns)
* @returns New transposed keyboard
*/
toTransposed(): Keyboard;
/**
* Flow buttons into columns
* @param columns - Number of columns
* @param options - Flow options
* @returns New flowed keyboard
*/
toFlowed(
columns: number,
options?: { wrap?: (row: number, index: number) => boolean }
): Keyboard;
/**
* Clone keyboard
* @param keyboard - Optional new keyboard layout
* @returns Cloned keyboard
*/
clone(keyboard?: KeyboardButton[][]): Keyboard;
/**
* Append other keyboards
* @param sources - Keyboards to append
* @returns This keyboard for chaining
*/
append(...sources: KeyboardSource[]): this;
/**
* Build final keyboard array
* @returns Keyboard button array
*/
build(): KeyboardButton[][];
// Static button factories
static text(text: string): KeyboardButton.CommonButton;
static requestUsers(text: string, requestId: number, options?: Omit<KeyboardButton.RequestUsersButton, "text" | "request_users">): KeyboardButton.RequestUsersButton;
static requestChat(text: string, requestId: number, options?: Omit<KeyboardButton.RequestChatButton, "text" | "request_chat">): KeyboardButton.RequestChatButton;
static requestContact(text: string): KeyboardButton.RequestContactButton;
static requestLocation(text: string): KeyboardButton.RequestLocationButton;
static requestPoll(text: string, type?: PollType): KeyboardButton.RequestPollButton;
static webApp(text: string, url: string): KeyboardButton.WebAppButton;
/**
* Create keyboard from source
* @param source - Keyboard source
* @returns New keyboard
*/
static from(source: KeyboardSource): Keyboard;
}Usage Examples:
import { Bot, Keyboard } from "grammy";
const bot = new Bot("TOKEN");
// Simple text buttons
const keyboard = new Keyboard()
.text("Button 1").text("Button 2").row()
.text("Button 3");
bot.command("start", (ctx) => {
return ctx.reply("Choose an option:", {
reply_markup: keyboard
});
});
// Request contact and location
const contactKeyboard = new Keyboard()
.requestContact("Share Contact")
.requestLocation("Share Location")
.oneTime();
bot.command("contact", (ctx) => {
return ctx.reply("Please share your info:", {
reply_markup: contactKeyboard
});
});
// Web app buttons
const webAppKeyboard = new Keyboard()
.webApp("Open App", "https://example.com/app");
// One-time keyboard with placeholder
const onceKeyboard = new Keyboard()
.text("Yes").text("No")
.oneTime()
.resized()
.placeholder("Choose yes or no");
// Using static methods
const buttons = [
[Keyboard.text("A"), Keyboard.text("B")],
[Keyboard.requestContact("Contact")]
];
// Flow into columns
const menuKeyboard = new Keyboard()
.text("1").text("2").text("3")
.text("4").text("5").text("6")
.toFlowed(2); // 3 rows of 2 columns
// Remove keyboard
bot.command("remove", (ctx) => {
return ctx.reply("Keyboard removed", {
reply_markup: { remove_keyboard: true }
});
});Keyboards with buttons attached to messages that send callback queries or URLs.
/**
* Builder for inline keyboards attached to messages
*/
class InlineKeyboard {
/**
* Create inline keyboard builder
* @param inline_keyboard - Optional initial button layout
*/
constructor(inline_keyboard?: InlineKeyboardButton[][]);
/**
* 2D array of inline keyboard buttons
*/
inline_keyboard: InlineKeyboardButton[][];
/**
* Add buttons to current row
* @param buttons - Buttons to add
* @returns This keyboard for chaining
*/
add(...buttons: InlineKeyboardButton[]): this;
/**
* Start new row with buttons
* @param buttons - Buttons for new row
* @returns This keyboard for chaining
*/
row(...buttons: InlineKeyboardButton[]): this;
/**
* Add URL button to current row
* @param text - Button text
* @param url - URL to open
* @returns This keyboard for chaining
*/
url(text: string, url: string): this;
/**
* Add callback button to current row
* @param text - Button text
* @param data - Callback data (default: text)
* @returns This keyboard for chaining
*/
text(text: string, data?: string): this;
/**
* Add web app button to current row
* @param text - Button text
* @param url - Web app URL or WebAppInfo
* @returns This keyboard for chaining
*/
webApp(text: string, url: string | WebAppInfo): this;
/**
* Add login button to current row
* @param text - Button text
* @param loginUrl - Login URL or LoginUrl config
* @returns This keyboard for chaining
*/
login(text: string, loginUrl: string | LoginUrl): this;
/**
* Add switch inline button (opens inline mode in any chat)
* @param text - Button text
* @param query - Inline query text (optional)
* @returns This keyboard for chaining
*/
switchInline(text: string, query?: string): this;
/**
* Add switch inline current button (opens inline mode in current chat)
* @param text - Button text
* @param query - Inline query text (optional)
* @returns This keyboard for chaining
*/
switchInlineCurrent(text: string, query?: string): this;
/**
* Add switch inline chosen button (opens inline mode with chat chooser)
* @param text - Button text
* @param query - SwitchInlineQueryChosenChat config (optional)
* @returns This keyboard for chaining
*/
switchInlineChosen(text: string, query?: SwitchInlineQueryChosenChat): this;
/**
* Add copy text button
* @param text - Button text
* @param copyText - Text to copy or CopyTextButton config
* @returns This keyboard for chaining
*/
copyText(text: string, copyText: string | CopyTextButton): this;
/**
* Add game button to current row
* @param text - Button text
* @returns This keyboard for chaining
*/
game(text: string): this;
/**
* Add pay button to current row
* @param text - Button text
* @returns This keyboard for chaining
*/
pay(text: string): this;
/**
* Transpose keyboard (swap rows and columns)
* @returns New transposed keyboard
*/
toTransposed(): InlineKeyboard;
/**
* Flow buttons into columns
* @param columns - Number of columns
* @param options - Flow options
* @returns New flowed keyboard
*/
toFlowed(
columns: number,
options?: { wrap?: (row: number, index: number) => boolean }
): InlineKeyboard;
/**
* Clone keyboard
* @returns Cloned keyboard
*/
clone(): InlineKeyboard;
/**
* Append other keyboards
* @param sources - Keyboards to append
* @returns This keyboard for chaining
*/
append(...sources: InlineKeyboardSource[]): this;
// Static button factories
static url(text: string, url: string): InlineKeyboardButton.UrlButton;
static text(text: string, data?: string): InlineKeyboardButton.CallbackButton;
static webApp(text: string, url: string | WebAppInfo): InlineKeyboardButton.WebAppButton;
static login(text: string, loginUrl: string | LoginUrl): InlineKeyboardButton.LoginButton;
static switchInline(text: string, query?: string): InlineKeyboardButton.SwitchInlineButton;
static switchInlineCurrent(text: string, query?: string): InlineKeyboardButton.SwitchInlineCurrentChatButton;
static switchInlineChosen(text: string, query?: SwitchInlineQueryChosenChat): InlineKeyboardButton.SwitchInlineChosenChatButton;
static copyText(text: string, copyText: string | CopyTextButton): InlineKeyboardButton.CopyTextButtonButton;
static game(text: string): InlineKeyboardButton.GameButton;
static pay(text: string): InlineKeyboardButton.PayButton;
/**
* Create keyboard from source
* @param source - Keyboard source
* @returns New inline keyboard
*/
static from(source: InlineKeyboardSource): InlineKeyboard;
}Usage Examples:
import { Bot, InlineKeyboard } from "grammy";
const bot = new Bot("TOKEN");
// Callback buttons
const menu = new InlineKeyboard()
.text("Button 1", "callback_1")
.text("Button 2", "callback_2").row()
.text("Button 3", "callback_3");
bot.command("menu", (ctx) => {
return ctx.reply("Choose an option:", {
reply_markup: menu
});
});
// Handle callbacks
bot.callbackQuery("callback_1", (ctx) => {
return ctx.answerCallbackQuery("You clicked button 1!");
});
// URL buttons
const links = new InlineKeyboard()
.url("Visit Website", "https://example.com")
.url("GitHub", "https://github.com");
// Mixed button types
const keyboard = new InlineKeyboard()
.url("Website", "https://example.com").row()
.text("Action 1", "action_1").text("Action 2", "action_2").row()
.switchInline("Share", "Check this out!");
// Pagination
function getPaginationKeyboard(page: number, totalPages: number) {
const keyboard = new InlineKeyboard();
if (page > 1) {
keyboard.text("◀️ Prev", `page_${page - 1}`);
}
keyboard.text(`${page}/${totalPages}`, "current");
if (page < totalPages) {
keyboard.text("Next ▶️", `page_${page + 1}`);
}
return keyboard;
}
bot.callbackQuery(/^page_(\d+)$/, (ctx) => {
const page = parseInt(ctx.match[1]);
return ctx.editMessageReplyMarkup({
reply_markup: getPaginationKeyboard(page, 10)
});
});
// Dynamic keyboard
bot.command("options", (ctx) => {
const options = ["Option A", "Option B", "Option C"];
const keyboard = new InlineKeyboard();
options.forEach((option, i) => {
keyboard.text(option, `option_${i}`);
if (i < options.length - 1) keyboard.row();
});
return ctx.reply("Select an option:", {
reply_markup: keyboard
});
});
// Flow into columns
const numberKeyboard = new InlineKeyboard()
.text("1", "1").text("2", "2").text("3", "3")
.text("4", "4").text("5", "5").text("6", "6")
.text("7", "7").text("8", "8").text("9", "9")
.toFlowed(3); // 3x3 grid
// Payment button
const paymentKeyboard = new InlineKeyboard()
.pay("Pay $9.99");
// Game button
const gameKeyboard = new InlineKeyboard()
.game("Play Game");
// Using static methods
const staticKeyboard = new InlineKeyboard()
.add(
InlineKeyboard.url("Link", "https://example.com"),
InlineKeyboard.text("Button", "data")
);interface KeyboardButton {
text: string;
request_users?: KeyboardButtonRequestUsers;
request_chat?: KeyboardButtonRequestChat;
request_contact?: boolean;
request_location?: boolean;
request_poll?: KeyboardButtonPollType;
web_app?: WebAppInfo;
}
namespace KeyboardButton {
interface CommonButton {
text: string;
}
interface RequestUsersButton {
text: string;
request_users: KeyboardButtonRequestUsers;
}
interface RequestChatButton {
text: string;
request_chat: KeyboardButtonRequestChat;
}
interface RequestContactButton {
text: string;
request_contact: true;
}
interface RequestLocationButton {
text: string;
request_location: true;
}
interface RequestPollButton {
text: string;
request_poll: KeyboardButtonPollType;
}
interface WebAppButton {
text: string;
web_app: WebAppInfo;
}
}
interface InlineKeyboardButton {
text: string;
url?: string;
callback_data?: string;
web_app?: WebAppInfo;
login_url?: LoginUrl;
switch_inline_query?: string;
switch_inline_query_current_chat?: string;
switch_inline_query_chosen_chat?: SwitchInlineQueryChosenChat;
copy_text?: CopyTextButton;
callback_game?: CallbackGame;
pay?: boolean;
}
namespace InlineKeyboardButton {
interface UrlButton {
text: string;
url: string;
}
interface CallbackButton {
text: string;
callback_data: string;
}
interface WebAppButton {
text: string;
web_app: WebAppInfo;
}
interface LoginButton {
text: string;
login_url: LoginUrl;
}
interface SwitchInlineButton {
text: string;
switch_inline_query: string;
}
interface SwitchInlineCurrentChatButton {
text: string;
switch_inline_query_current_chat: string;
}
interface SwitchInlineChosenChatButton {
text: string;
switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat;
}
interface CopyTextButtonButton {
text: string;
copy_text: CopyTextButton;
}
interface GameButton {
text: string;
callback_game: CallbackGame;
}
interface PayButton {
text: string;
pay: true;
}
}
type KeyboardSource = Keyboard | KeyboardButton[][];
type InlineKeyboardSource = InlineKeyboard | InlineKeyboardButton[][];
interface WebAppInfo {
url: string;
}
interface LoginUrl {
url: string;
forward_text?: string;
bot_username?: string;
request_write_access?: boolean;
}
interface SwitchInlineQueryChosenChat {
query?: string;
allow_user_chats?: boolean;
allow_bot_chats?: boolean;
allow_group_chats?: boolean;
allow_channel_chats?: boolean;
}
interface CopyTextButton {
text: string;
}
interface PollType {
type?: "quiz" | "regular";
}Install with Tessl CLI
npx tessl i tessl/npm-grammy