Inline keyboards are one of the most powerful features of Telegram bots. They let you add clickable buttons directly below a message, turning a simple text response into an interactive interface. Users can navigate menus, confirm actions, pick options, or trigger workflows — all without typing a single command.
This guide covers everything you need to know about inline keyboards: how the JSON structure works, limits and best practices, and ready-to-use code examples in Python.
An inline keyboard is a JSON object attached to a message via the reply_markup parameter. It contains an array of rows, where each row is an array of buttons. Each button has a visible text label and an action — most commonly a callback_data string that your bot receives when the user taps the button.
{
"inline_keyboard": [
[
{"text": "Yes ✅", "callback_data": "confirm_yes"},
{"text": "No ❌", "callback_data": "confirm_no"}
],
[
{"text": "Cancel", "callback_data": "cancel"}
]
]
}
This creates two rows: the first with "Yes" and "No" side by side, and the second with a full-width "Cancel" button below them.
Telegram supports several button types beyond simple callbacks:
| Type | Field | Use case |
|---|---|---|
| Callback | callback_data | Triggers a callback query your bot handles. Max 64 bytes. |
| URL | url | Opens a link in the user's browser. |
| Switch Inline | switch_inline_query | Prompts user to pick a chat and starts inline query. |
| Web App | web_app | Opens a Telegram Mini App (WebApp). |
| Login | login_url | Telegram Login widget for websites. |
| Pay | pay | Payment button (must be first button in first row). |
Telegram enforces a few constraints on inline keyboards that are worth knowing before you build complex layouts:
callback_data is limited to 1–64 bytes. Keep your identifiers short. Instead of user_clicked_the_settings_menu_item, use settings or s:1. You can store extra state in your database and just pass an ID in the callback.
Each row can hold up to 8 buttons. If you add more, Telegram will reject the message. In practice, 3–4 buttons per row works best for readability on mobile screens.
There's no official limit on the number of rows, but Telegram recommends keeping keyboards compact. A keyboard with 10+ rows pushes the message content off-screen and creates a poor UX. If you need many options, consider pagination.
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
Application, CommandHandler,
CallbackQueryHandler, ContextTypes,
)
async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = [
[
InlineKeyboardButton("📊 Stats", callback_data="stats"),
InlineKeyboardButton("⚙️ Settings", callback_data="settings"),
],
[InlineKeyboardButton("❓ Help", callback_data="help")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("Choose an option:", reply_markup=reply_markup)
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer() # always answer callback queries
await query.edit_message_text(f"You selected: {query.data}")
query.answer() even if you don't want to show a notification. If you don't, Telegram shows a loading spinner on the button for 30 seconds.
Use emoji at the start of button labels. It makes buttons scannable at a glance. Users process icons faster than text — "📊 Stats" reads faster than "View Statistics".
Group related actions in the same row. Put "Yes" and "No" side by side, not stacked vertically. Put destructive actions like "Delete" on their own row, preferably at the bottom.
Keep callback_data structured. Use a prefix convention like menu:main, menu:settings, page:2. This makes it easier to route callbacks with pattern matching instead of long if/else chains.
Update the keyboard after a tap instead of sending a new message. Use editMessageText or editMessageReplyMarkup to change the buttons in place. This keeps the chat clean and feels more app-like.
Build your keyboard visually — no coding needed. Add buttons, set callback_data, and export the code in JSON, Python, format.
Try the Inline Keyboard Builder →