Recommended resources
The freely available resources this lesson will direct you to.
Welcome back! In the previous lesson, we took our first step toward creating interactive bots by implementing reply keyboards. You learned how to replace the user's standard keyboard with custom buttons that send pre-defined text messages. This is great for simple, guided conversations.
Now, we're going to unlock a much more powerful form of interaction that is central to building the sophisticated "tech UIs" you're aiming for. This lesson introduces inline keyboards, which are buttons attached directly to a message. Unlike reply keyboards, these buttons send silent, invisible data back to your bot, allowing for the creation of dynamic menus, settings panels, and navigation systems without cluttering the chat history.
By the end of this lesson, you will be able to create and handle these inline keyboards, which form the backbone of almost every advanced Telegram bot interface.
Reply vs. Inline: A Critical Distinction
Before we start building, it's essential to solidify the conceptual difference between the keyboards we've covered and the ones we're about to learn.
In the last lesson, we used the Keyboard class. Tapping a button sent a text message, which we handled with bot.hears().
Today, we'll use the InlineKeyboard class. These keyboards appear within the chat, attached to a specific message bubble. When a user taps an inline button, the Telegram client doesn't send a text message. Instead, it sends a special type of update called a callback query. This query contains a small piece of data—a string payload—that you define when creating the button.
This mechanism is what allows you to build complex UIs. The user can navigate through menus by tapping buttons, and your bot can react to these data payloads without any new text messages appearing in the chat.

Let's start by reviewing the grammY documentation which provides an excellent summary of these differences.
Inline and Custom Keyboards (built-in) - grammY
This initial section of the grammY keyboard guide clearly distinguishes between the two keyboard types and introduces the key terminology.
Read the introductory section up to the "Inline Keyboards" heading. Pay close attention to the table that contrasts Inline Keyboard and Custom Keyboard, and specifically the definition of an Inline Keyboard button and the "callback query" it sends.
Building and Sending an Inline Keyboard
As you've just read, grammY provides the InlineKeyboard class for creating these interfaces. The API is very similar to the Keyboard builder you've already used, making it intuitive to pick up.
First, you'll need to import the class from grammY:
import { Bot, InlineKeyboard } from "grammy";
Now, let's create a simple inline keyboard. The primary method we'll use is .text(), but with a crucial difference: it takes a second argument, which is the callback data payload.
.text(label: string, callback_data: string)
Let's build a simple "control panel" for a hypothetical service.
// Create an inline keyboard
const controlPanel = new InlineKeyboard()
.text("▶️ Start", "start_process").text("⏹️ Stop", "stop_process").row()
.text("🔄 Restart", "restart_process");
// Register a command to show the keyboard
bot.command("controls", async (ctx) => {
await ctx.reply("Service Control Panel:", {
reply_markup: controlPanel,
});
});
Let's analyze this code:
- We instantiate a
new InlineKeyboard(). - We add a "▶️ Start" button. The visible label is
"▶️ Start", but the data payload sent to our bot will be the string"start_process". - We add "⏹️ Stop" and "🔄 Restart" buttons, each with their own unique callback data.
- We use
.row()to structure the layout, just as we did with reply keyboards. - Finally, in our command handler, we send the keyboard using the
reply_markupoption inctx.reply().
To see this in more detail, let's turn to the documentation.
Inline and Custom Keyboards (built-in) - grammY
This part of the guide details the InlineKeyboard class, the builder pattern, and the method for sending it.
Read the sections Building an Inline Keyboard and Sending an Inline Keyboard. Focus on the syntax for .text(label, data) and how the resulting keyboard object is passed to ctx.reply.
Handling Callback Queries
You've sent the keyboard, and the user can see the buttons. Now, how do you react when they press one?
Since button clicks generate callback_query updates, bot.hears() won't work. Instead, grammY provides a dedicated listener: bot.callbackQuery().
This listener works similarly to bot.command() or bot.hears(). You provide it with the specific callback_data string you want to listen for, and a handler function to execute.
Let's add handlers for the control panel we just created.
// Handle the 'start_process' callback query
bot.callbackQuery("start_process", async (ctx) => {
// The user's client will show a loading animation until this is called.
await ctx.answerCallbackQuery();
await ctx.reply("Starting the process...");
});
// Handle the 'stop_process' callback query
bot.callbackQuery("stop_process", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.reply("Stopping the process...");
});
// Handle the 'restart_process' callback query
bot.callbackQuery("restart_process", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.reply("Restarting the process...");
});
Here's the crucial part:
- We use
bot.callbackQuery("start_process", ...)to specifically catch clicks on the "Start" button. - Inside the handler,
ctxis aContextobject that now contains information about the callback query inctx.callbackQuery. - You'll notice the
await ctx.answerCallbackQuery()call. This is important! It sends an acknowledgement to Telegram confirming that you've received the button click. This stops the loading indicator on the user's button. We will explore this method in-depth in the next lesson, but for now, know that you should always call it to provide a good user experience.
To see the official explanation for this, let's consult the final part of the documentation for this topic.
Inline and Custom Keyboards (built-in) - grammY
This section explains the mechanism for handling the updates generated by inline button presses.
Read the section Responding to Inline Keyboard Clicks. Focus on the use of bot.callbackQuery() to listen for specific data payloads and the example code provided. Also note the mention of bot.on("callback_query:data") as a way to catch any button click.
Putting It All Together
Let's combine everything into a single, runnable example. This bot will present a simple choice to the user and react based on their selection.
import { Bot, Context, InlineKeyboard } from "grammy";
// Assume your bot token is in an environment variable
const bot = new Bot(process.env.BOT_TOKEN || "");
// Create the inline keyboard
const decisionKeyboard = new InlineKeyboard()
.text("Choose Path A", "path_a")
.text("Choose Path B", "path_b");
// Register the /start command to send the keyboard
bot.command("start", async (ctx) => {
await ctx.reply("You stand at a crossroads. Which path will you take?", {
reply_markup: decisionKeyboard,
});
});
// Handle the 'path_a' callback query
bot.callbackQuery("path_a", async (ctx) => {
// Acknowledge the query to remove the loading spinner
await ctx.answerCallbackQuery();
// Send a new message in response
await ctx.reply("You chose Path A. A wise decision!");
});
// Handle the 'path_b' callback query
bot.callbackQuery("path_b", async (ctx) => {
await ctx.answerCallbackQuery();
await ctx.reply("You chose Path B. An adventurous choice!");
});
// Start the bot
bot.start();
console.log("Bot started!");
I encourage you to run this code. You'll see how tapping the buttons triggers the corresponding callbackQuery handler and sends a new message, all without sending any text input from your side.
Conclusion
You have now mastered the second, and arguably more powerful, type of keyboard in the Telegram Bot API. Inline keyboards are the foundation for creating dynamic and intuitive user interfaces, from simple confirmation dialogs to complex, multi-layered menu systems.
Key Takeaways:
- Inline keyboards are attached directly to messages and do not replace the user's keyboard.
- Their buttons send callback queries containing a data payload, not visible text messages.
- You create them using the
InlineKeyboardclass and the.text(label, data)method. - You handle button clicks by listening for specific payloads with
bot.callbackQuery("your_data_here", ...). - It is essential to call
ctx.answerCallbackQuery()inside your handler to acknowledge the click and provide a responsive user experience.
In our next lesson, we will dive deeper into answerCallbackQuery. You'll learn how to not just acknowledge a click, but also display pop-up notifications or alerts to the user, providing immediate and contextual feedback. This will set the stage for editing messages, allowing you to build truly dynamic menus that update in place.