| name | telegram-keyboard-design |
| description | Design Telegram bot keyboards, buttons, and conversational flows with proper UX patterns. Includes inline keyboards, reply keyboards, button callbacks, and progressive disclosure for better mobile UX. |
Telegram Keyboard Design Skill
When to Use
Use this skill when:
- Designing Telegram bot menus and navigation
- Creating inline buttons for product browsing
- Building conversational flows with progressive disclosure
- Implementing callback handlers for button interactions
- Designing mobile-first keyboard layouts
- Managing state in bot conversations
- Creating rich interaction patterns
Design Principles
1. Progressive Disclosure
Only show relevant buttons at each step, reducing cognitive load:
Bad: Show 20 buttons at once
Good: Show 3-5 buttons, then reveal more based on choice
2. Mobile-First Design
- Buttons are small touch targets
- Stack vertically when possible (2-column max)
- Avoid too many rows (5 max per screen)
3. Consistent Navigation
- Always include a "Back" button for multi-step flows
- "Main Menu" to return home
- Clear CTA buttons
Code Patterns
1. Reply Keyboard (Main Menu)
from telegram import ReplyKeyboardMarkup, KeyboardButton
def get_main_keyboard():
"""Main menu with 2-column layout"""
keyboard = [
[
KeyboardButton("🛍️ Buscar Ofertas"),
KeyboardButton("💰 Meus Cupons")
],
[
KeyboardButton("❓ Ajuda"),
KeyboardButton("⚙️ Configurações")
]
]
return ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True, # Auto-size buttons
one_time_keyboard=False # Keep keyboard visible
)
2. Inline Keyboard (Button Actions)
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
def get_product_keyboard(product_id: int, page: int = 1):
"""Product view with action buttons"""
keyboard = [
[
InlineKeyboardButton("📋 Detalhes", callback_data=f"product_details_{product_id}"),
InlineKeyboardButton("🔗 Link", url=f"https://ofertachina.com/p/{product_id}")
],
[
InlineKeyboardButton("❤️ Salvar", callback_data=f"save_product_{product_id}"),
InlineKeyboardButton("📤 Compartilhar", callback_data=f"share_product_{product_id}")
],
[
InlineKeyboardButton("⬅️ Voltar", callback_data="back_to_products")
]
]
return InlineKeyboardMarkup(keyboard)
def get_pagination_keyboard(page: int, total_pages: int):
"""Pagination buttons"""
keyboard = []
buttons = []
if page > 1:
buttons.append(InlineKeyboardButton("⬅️ Anterior", callback_data=f"page_{page-1}"))
buttons.append(InlineKeyboardButton(f"{page}/{total_pages}", callback_data="noop"))
if page < total_pages:
buttons.append(InlineKeyboardButton("Próxima ➡️", callback_data=f"page_{page+1}"))
keyboard.append(buttons)
return InlineKeyboardMarkup(keyboard)
3. Category Selection Flow
# Progressive disclosure: First show categories, then products
def get_category_keyboard():
"""First step: Choose category"""
keyboard = [
[InlineKeyboardButton("🖥️ Eletrônicos", callback_data="cat_electronics")],
[InlineKeyboardButton("👗 Moda", callback_data="cat_fashion")],
[InlineKeyboardButton("🏠 Casa", callback_data="cat_home")],
[InlineKeyboardButton("🎮 Games", callback_data="cat_games")],
[InlineKeyboardButton("📚 Livros", callback_data="cat_books")],
[InlineKeyboardButton("🔄 Voltar", callback_data="main_menu")]
]
return InlineKeyboardMarkup(keyboard)
def get_subcategory_keyboard(category: str):
"""Second step: Choose subcategory within category"""
subcategories = {
"electronics": [
("📱 Smartphones", "subcat_phones"),
("💻 Laptops", "subcat_laptops"),
("⌚ Wearables", "subcat_wearables"),
],
"fashion": [
("👔 Homem", "subcat_mens"),
("👗 Mulher", "subcat_womens"),
("👶 Infantil", "subcat_kids"),
]
}
keyboard = []
for label, callback in subcategories.get(category, []):
keyboard.append([InlineKeyboardButton(label, callback_data=callback)])
keyboard.append([InlineKeyboardButton("⬅️ Voltar", callback_data="categories")])
return InlineKeyboardMarkup(keyboard)
4. Callback Handler
from telegram import Update
from telegram.ext import ContextTypes, CallbackQueryHandler
async def handle_product_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle button callbacks"""
query = update.callback_query
await query.answer() # Remove loading spinner
callback_data = query.data
if callback_data.startswith("product_details_"):
product_id = int(callback_data.split("_")[-1])
await show_product_details(query, product_id, context)
elif callback_data.startswith("save_product_"):
product_id = int(callback_data.split("_")[-1])
await save_to_favorites(query, product_id, context)
elif callback_data.startswith("page_"):
page = int(callback_data.split("_")[-1])
await show_page(query, page, context)
elif callback_data == "main_menu":
await query.edit_message_text(
text="🏠 Menu Principal",
reply_markup=get_main_keyboard()
)
async def show_product_details(query, product_id: int, context: ContextTypes.DEFAULT_TYPE):
"""Show detailed product view"""
# Fetch product from database
product = await get_product(product_id)
text = f"""
📦 *{product['title']}*
💰 Preço: R$ {product['price']:.2f}
⭐ Avaliação: {product['rating']}/5
📦 Envio: Frete grátis
{product['description']}
🔗 [Ver na loja]({product['url']})
""".strip()
await query.edit_message_text(
text=text,
reply_markup=get_product_keyboard(product_id),
parse_mode="Markdown"
)
async def save_to_favorites(query, product_id: int, context: ContextTypes.DEFAULT_TYPE):
"""Toggle favorite status"""
user_id = query.from_user.id
# Save to database
saved = await toggle_favorite(user_id, product_id)
status = "✅ Salvo!" if saved else "❌ Removido"
await query.answer(status, show_alert=False)
5. Search with Buttons
from telegram import Update
from telegram.ext import ContextTypes, MessageHandler, filters
async def handle_search(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle search input"""
search_query = update.message.text
# Search products
products = await search_products(search_query)
if not products:
await update.message.reply_text("❌ Nenhum produto encontrado")
return
# Show first 5 products with next page button
text = "🔍 Resultados da busca:\n\n"
keyboard = []
for i, product in enumerate(products[:5], 1):
text += f"{i}. {product['title']}\n💰 R$ {product['price']}\n\n"
keyboard.append([
InlineKeyboardButton(
f"#{i} Ver",
callback_data=f"product_details_{product['id']}"
)
])
# Pagination if more results
if len(products) > 5:
keyboard.append([
InlineKeyboardButton("Próxima página ➡️", callback_data="search_next_page")
])
keyboard.append([InlineKeyboardButton("🔄 Nova busca", callback_data="main_menu")])
await update.message.reply_text(
text=text,
reply_markup=InlineKeyboardMarkup(keyboard),
parse_mode="HTML"
)
6. Confirmation Dialog
def get_confirm_keyboard(action_id: str):
"""Confirmation buttons"""
keyboard = [
[
InlineKeyboardButton("✅ Confirmar", callback_data=f"confirm_{action_id}"),
InlineKeyboardButton("❌ Cancelar", callback_data="cancel")
]
]
return InlineKeyboardMarkup(keyboard)
async def handle_share(query, product_id: int, context: ContextTypes.DEFAULT_TYPE):
"""Share product confirmation"""
product = await get_product(product_id)
text = f"""
Compartilhar este produto?
📦 {product['title']}
💰 R$ {product['price']}
""".strip()
await query.edit_message_text(
text=text,
reply_markup=get_confirm_keyboard(f"share_{product_id}")
)
Mobile UX Checklist
✅ Buttons are 30-40px tall (easy to tap)
✅ Max 2 buttons per row horizontally
✅ Max 5 rows visible without scrolling
✅ Back buttons always available
✅ Loading state with query.answer()
✅ Edit, not reply for follow-up messages
✅ Emoji for visual clarity
✅ Short labels (max 20 chars)
Anti-Patterns ❌
❌ Show 20 buttons at once
❌ Nested buttons without back
❌ Tiny buttons (<25px)
❌ No loading feedback (frozen UI)
❌ Same text with different actions
❌ Deep button hierarchies (>3 levels)
Related Files
- keyboard-templates.py - Ready-to-use keyboard builders
- callback-handlers.py - Callback handler examples
- flow-patterns.md - Common conversation flows
References
- python-telegram-bot: https://python-telegram-bot.readthedocs.io/
- Telegram Bot API: https://core.telegram.org/bots/api#inlinekeyboardmarkup
- UX Best Practices: https://core.telegram.org/bots/design