How to Navigate in a Telegram Bot: Inline buttons, reply menus, commands and Mini App
Main chat
A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.
Navigation in a Telegram bot is a set of interface elements with which the user moves between the bot sections, launches the desired functions and receives answers without having to print something manually.
Good navigation in a bot works on one principle: the user always sees what can be done next, and gets to the desired action in no more than two or three clicks.
Telegram has several types of navigation elements:
| Элемент | Где отображается | Главная особенность |
|---|---|---|
| Inline-кнопки | Под конкретным сообщением | Привязаны к сообщению, меняются динамически |
| Reply-клавиатура | Над полем ввода | Постоянно видны, заменяют клавиатуру |
| Команды | Меню «/» у поля ввода | Работают всегда, из любого состояния |
| Кнопка Menu | Слева от поля ввода | Открывает список команд или Mini App |
| Mini App | Полноэкранный веб-интерфейс | Полноценный сайт внутри Telegram |
Inline buttons: the most flexible type of navigation
What it is and when to use it
Inline buttons are attached to a particular message or media file and stay close to it. This is the most popular type of navigation in modern bots – they do not overlap the screen, appear where necessary, and can change in response to user actions.
Use the inline buttons when:
- you need to offer answers at a specific point in the dialogue
- pagination is required (list of products, articles, results)
- confirm/Cancel buttons for dangerous activities
- you want to update the content of the message without sending a new one
Inline buttons in Python (aiogram 3)
from aiogram import Bot, Dispatcher, F
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.filters import CommandStart
bot = Bot(token="YOUR_BOT_TOKEN")
dp = Dispatcher()
# Главное меню
def main_menu() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📦 Каталог", callback_data="catalog"),
InlineKeyboardButton(text="🛒 Корзина", callback_data="cart"),
],
[
InlineKeyboardButton(text="📞 Поддержка", callback_data="support"),
InlineKeyboardButton(text="⚙️ Настройки", callback_data="settings"),
],
])
@dp.message(CommandStart())
async def start(message):
await message.answer(
"Привет! Выберите раздел:",
reply_markup=main_menu()
)
# Обработка нажатий
@dp.callback_query(F.data == "catalog")
async def show_catalog(callback: CallbackQuery):
# Обновляем сообщение вместо отправки нового
await callback.message.edit_text(
"📦 Каталог товаров:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="← Назад", callback_data="back_main")],
])
)
await callback.answer() # убираем "часики" на кнопке
@dp.callback_query(F.data == "back_main")
async def back_to_main(callback: CallbackQuery):
await callback.message.edit_text(
"Главное меню:",
reply_markup=main_menu()
)
await callback.answer()
Inline buttons on Node.js (grammY)
const { Bot, InlineKeyboard } = require('grammy');
const bot = new Bot('YOUR_BOT_TOKEN');
// Функция создания главного меню
function mainMenu() {
return new InlineKeyboard()
.text('📦 Каталог', 'catalog').text('🛒 Корзина', 'cart').row()
.text('📞 Поддержка', 'support').text('⚙️ Настройки', 'settings');
}
bot.command('start', async (ctx) => {
await ctx.reply('Привет! Выберите раздел:', {
reply_markup: mainMenu(),
});
});
bot.callbackQuery('catalog', async (ctx) => {
await ctx.editMessageText('📦 Каталог товаров:', {
reply_markup: new InlineKeyboard().text('← Назад', 'back_main'),
});
await ctx.answerCallbackQuery();
});
bot.callbackQuery('back_main', async (ctx) => {
await ctx.editMessageText('Главное меню:', {
reply_markup: mainMenu(),
});
await ctx.answerCallbackQuery();
});
Link button in the inline menu
An inline button can open not just an action, but an external link or site:
InlineKeyboardButton(
text="🌐 Открыть сайт",
url="https://your-site.ru"
)
Reply keyboard: permanent menu under the input field
What it is and when to use it
Reply buttons look like template pre-prepared answers and are fixed instead of the main keyboard on the device screen. Usually used in chatbots as the main menu.
They always remain under the input field, are clearly visible and occupy part of the screen, but they have a significant disadvantage: they can easily get lost and disappear. Users sometimes accidentally remove the keyboard swipe and do not understand where it went.
Use the reply keyboard when:
- bot simple with a fixed set of actions
- the audience is not technically savvy, you need constantly visible buttons
- navigation does not change depending on the context
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove
def main_reply_keyboard() -> ReplyKeyboardMarkup:
return ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="📦 Каталог"), KeyboardButton(text="🛒 Корзина")],
[KeyboardButton(text="📞 Поддержка"), KeyboardButton(text="ℹ️ О нас")],
],
resize_keyboard=True, # компактный размер
one_time_keyboard=False # не скрывать после нажатия
)
@dp.message(CommandStart())
async def start(message):
await message.answer(
"Выберите раздел:",
reply_markup=main_reply_keyboard()
)
# Обработка нажатия по тексту кнопки
@dp.message(F.text == "📦 Каталог")
async def catalog(message):
await message.answer("Открываю каталог...")
# Убрать клавиатуру когда не нужна
@dp.message(F.text == "Скрыть меню")
async def hide_keyboard(message):
await message.answer(
"Клавиатура скрыта",
reply_markup=ReplyKeyboardRemove()
)
Commands: Navigation through /start, /help, /menu
What is it and why
To use the menu, you need to click on the icon to the left of the message input field or write a slash - the bot will tell the available commands. Such commands have the highest priority: they will be executed even if the user was in another chain.
Commands are the safety net of navigation. Even if the user gets confused in the buttons or loses the menu, /start will always return it to the beginning.
Set up commands through BotFather
/setcommands
Choose a bot
Enter the list:
start - Getting started
menu - Main menu
catalog - Product catalog
Cart - My basket
help
settings - Settings
After that, the user sees command prompts when entering /.
Processing commands in code
from aiogram.filters import command
@dp.message(Command("menu")
async def show menu(message):
wait message.answer("Main menu:", reply markup=main menu())
@dp.message(Command("help"))
async def help command(message):
await message.answer.
"Available commands:\n"
"/start is the beginning of work\n"
"/menu is the main menu\n"
"/catalog - directory\n"
"/cart - basket\n"
"/help is this reference"
)
Menu button: fixed input to the bot
The Menu button is the button to the left of the message input field (the grid icon or arrow icon). It opens a command list or Mini App. Configured via BotFather by the /setmenubutton command.
Two modes:
- List of commands - opens the same command menu with a slash
- Mini App - opens the web application directly from the Menu button
# Программная установка кнопки Menu через Bot API
from aiogram.types import MenuButtonWebApp, WebAppInfo
await bot.set_chat_menu_button(
menu_button=MenuButtonWebApp(
text="Открыть приложение",
web_app=WebAppInfo(url="https://your-site.ru/app")
)
)
Mini App: a full-fledged interface inside Telegram
Mini App is a web application (HTML/CSS/JS) that opens inside Telegram as a full-screen interface. Suitable when logic is too complex for buttons: a catalog with filters, an order form, a personal account.
<!-- Минимальная структура Mini App -->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
<script>
const tg = window.Telegram.WebApp;
tg.ready(); // инициализация
// Цвет интерфейса из темы Telegram
document.body.style.background = tg.themeParams.bg_color;
// Главная кнопка внизу экрана
tg.MainButton.setText('Оформить заказ');
tg.MainButton.show();
tg.MainButton.onClick(() => {
// Отправляем данные в бота
tg.sendData(JSON.stringify({ action: 'order', items: [] }));
});
</script>
</body>
</html>
How to build the right navigation structure
Good navigation in the Telegram bot is based on three rules.
**Depth rule. ** No more than three levels of nesting: main menu → section → action. If deeper, the user is lost and does not return.
**Back button rule. ** Each level has a return button deeper than the first. The user must not poke /start to get out of the subsection.
Rule /start. /start always goes back to the beginning. This is an absolute reset that must work from any state.
# Универсальная структура с кнопкой Назад
def catalog_menu() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👕 Одежда", callback_data="cat_clothes")],
[InlineKeyboardButton(text="👟 Обувь", callback_data="cat_shoes")],
[InlineKeyboardButton(text="🎒 Аксессуары", callback_data="cat_accessories")],
[InlineKeyboardButton(text="← Главное меню", callback_data="back_main")],
])
Comparison: what to choose for the task
| Задача | Рекомендуемый тип |
|---|---|
| Простой бот с 3–5 разделами | Reply-клавиатура |
| Многоуровневая навигация | Inline-кнопки |
| Выбор из списка / пагинация | Inline-кнопки |
| Подтверждение действия | Inline-кнопки |
| Кнопки с ссылками на сайт | Inline-кнопки с URL |
| Быстрый доступ из любого места | Команды через BotFather |
| Сложный интерфейс с формами | Mini App |
| Корзина и оплата | Mini App |
Frequent errors in the navigation of Telegram bots
Too many buttons in a row. Telegram displays inline buttons adaptively, but more than 3 buttons in one line is ugly on mobile. Optimally 1-2 buttons in a line.
No back button. The user has logged into the subsection and does not know how to return. Conversion losses are guaranteed.
Reply keyboard is hidden by accident. Add the command /menu which always shows the keyboard again. Users who accidentally hid it will find a way to get it back.
**Callback data is not unique. If two buttons are assigned the same callback_data, they will do the same thing. Use the action:id pattern: cat:12, page:3.
# Good: Unique identifiers with context
InlineKeyboardButton(text="Next →", callback data="page:2")
InlineKeyboardButton(text="Product 15", callback data="item:15")
# Bad: Same for different actions
InlineKeyboardButton(text="Next →", callback data="next")
InlineKeyboardButton(text="Back", callback data="back")
If you use several levels, the back will work unpredictably
No answerCallbackQuery. If you do not call callback.answer() after processing, the user will turn the "clock" on the button for several seconds. Always respond to a callback, even with a blank answer.
Navigation checklist in Telegram-bot
Structure:
● No more than 3 levels of nesting
● Each level (except the first) has a back button.
/start always returns to the main menu
/help shows a list of available commands
Inline buttons:
● No more than 2-3 buttons in one line
Callback data is unique and has an action:id pattern.
Each handler calls callback.answer()
Edit message is used instead of sending a new message where possible.
Teams:
● The list of commands is given through BotFather (/setcommands).
Commands are documented briefly and clearly
Reply keyboard:
Resize keyboard=True for compact display
● There is a way to restore the keyboard if the user has hidden it
Outcome
Navigation in the Telegram-bot is built from four tools: inline-buttons for dynamic scripts, reply-keyboard for permanent menu, commands as an emergency exit from any state, Mini App for complex interfaces.
For most projects, inline buttons plus three or four commands via BotFather are enough. Reply keyboard is suitable for simple bots with a permanent structure. Mini App – when logic outgrown the capabilities of the button interface.
The main principle is that the user should never get stuck: the next step is always visible and there is always a way back.
FAQ
**Is it possible to use inline buttons and reply keyboards at the same time? ** Yeah. When using inline buttons, the user sees not only them, but also the main keyboard. This is normal practice: reply-keyboard for the main menu, inline-buttons for contextual actions within the dialogue.
**How many buttons can the maximum be set? ** Technically, up to 100 inline buttons per message. Practically – no more than 8-10: no longer fits on the screen and confuses the user.
**How to make a button that opens the site? **
Inline button with url instead of callback_data. Clicking opens an external link in the browser or Mini App if web_app is used.
**Is it possible to change buttons after sending? **
Yes, via edit_message_reply_markup (keyboard only) or edit_message_text (text and keyboard). This is the key advantage of the inline buttons over the reply keyboard.
**What happens if the user loses the reply keyboard? **
Add a /menu command that sends a new message from reply_markup=main_reply_keyboard(). You can also add an inline “Show menu” button in the welcome message.
*Relevant to aiogram 3.x, grammY 1.x, Bot API 9.x. June 2026. *