telegram-keyboard-design

majiayu000's avatarfrom majiayu000

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.

⭐5stars🔀1forks📁View on GitHub🕐Updated Jan 11, 2026

When & Why to Use This Skill

This Claude skill specializes in architecting high-quality Telegram bot interfaces, focusing on mobile-first UX, intuitive keyboard layouts, and seamless conversational flows. It provides production-ready code patterns for inline and reply keyboards, ensuring developers can implement complex interactions like pagination, progressive disclosure, and state management while adhering to Telegram's best practices.

Use Cases

  • E-commerce & Catalog Browsing: Designing multi-column product menus with inline 'Buy' buttons and automated pagination for large inventories to enhance mobile shopping experiences.
  • Interactive Navigation Systems: Building hierarchical bot menus with consistent 'Back' and 'Home' buttons to prevent user friction and ensure clear navigation in deep conversational flows.
  • Data Collection & Onboarding: Implementing progressive disclosure patterns to collect user information step-by-step, significantly reducing cognitive load and improving form completion rates.
  • Dynamic Callback Handling: Creating robust backend logic to process button interactions, update message states in real-time, and provide instant visual feedback to users through callback queries.
nametelegram-keyboard-design
descriptionDesign 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)

References