MainButton & BackButton in Telegram Mini Apps — KEL IT
Telegram Mini Apps 6 min read

MainButton & BackButton: Native Navigation in Telegram Mini Apps

When users open a Mini App inside Telegram, they expect app-like behavior — a prominent action button at the bottom, a back arrow in the header, no floating HTML buttons that look out of place. Telegram provides exactly that through MainButton and BackButton, native WebApp controls rendered by the client, not your DOM.

In 2026, @telegram-apps/sdk-react is the recommended React wrapper. It exposes reactive helpers around window.Telegram.WebApp and saves you from manual event wiring. This guide covers production patterns: when to show each button, how to sync them with React Router, loading states, and why duplicating MainButton with your own sticky footer is usually a mistake.

Why Native Buttons Beat Custom UI

MainButton sits at the bottom of the Telegram viewport. It’s the standard place for “Confirm order”, “Pay now”, “Submit” — whatever the primary action is. Color comes from themeParams.button_color; you control text and enabled state.

BackButton appears in the Telegram header. It doesn’t eat layout space and feels consistent across Android, iOS, and Desktop. Hide it on root screens; show it on nested routes or modals.

Benefits over plain <button> elements:

  • Familiar UX — users already know MainButton from shops, games, and booking apps in Telegram.
  • Safe area handled — no guessing padding-bottom per device.
  • Haptic feedback on supported platforms.
  • Less “website in a box” — the app feels integrated with the messenger.

Setup with @telegram-apps/sdk-react

npm create vite@latest mini-app -- --template react-ts
cd mini-app
npm install @telegram-apps/sdk-react react-router-dom

Initialize in main.tsx:

import { init } from '@telegram-apps/sdk-react';
init();

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Call init() before any MainButton/BackButton usage. For local dev outside Telegram, use the SDK mock or open the app via a t.me test link.

Minimal MainButton example:

import { useEffect } from 'react';
import { mainButton } from '@telegram-apps/sdk-react';

export function Home() {
  useEffect(() => {
    mainButton.setParams({
      text: 'Go to checkout',
      isVisible: true,
      isEnabled: true,
    });

    const offClick = mainButton.onClick(() => {
      window.location.href = '/checkout';
    });

    return () => {
      offClick();
      mainButton.hide();
    };
  }, []);

  return <h1>Catalog</h1>;
}

Always clean up: unsubscribe from onClick and call mainButton.hide() on unmount. Otherwise stale handlers fire after route changes — one tap triggers multiple callbacks.

Building something similar? Message on Telegram.

MainButton States and UX Patterns

MainButton is a single shared control. Manage it explicitly:

StateHowUse case
HiddenmainButton.hide()Read-only screens, incomplete forms
EnabledisEnabled: trueValid form, ready to submit
DisabledisEnabled: falseMissing required fields
LoadingisLoaderVisible: trueAPI call in progress

Example with react-hook-form:

useEffect(() => {
  mainButton.setParams({
    text: isSubmitting ? 'Sending…' : 'Confirm order',
    isVisible: true,
    isEnabled: isValid && !isSubmitting,
    isLoaderVisible: isSubmitting,
  });
}, [isValid, isSubmitting]);

UX rules that matter in production:

  1. One MainButton per screen — there’s only one native slot. Secondary actions belong in content.
  2. Action-oriented labels — “Pay $19.99” beats “Next”. Keep text under ~25 characters.
  3. Don’t duplicate — either MainButton or a custom sticky button, not both saying “Checkout”.
  4. Hide when there’s nothing to do — catalog and profile pages don’t need a bottom CTA.

BackButton with React Router

Users need a predictable way back without building your own header. Hook pattern:

import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { backButton } from '@telegram-apps/sdk-react';

export function useTelegramBackButton(fallbackPath = '/') {
  const navigate = useNavigate();
  const { pathname } = useLocation();

  useEffect(() => {
    if (pathname === fallbackPath) {
      backButton.hide();
      return;
    }

    backButton.show();
    const offClick = backButton.onClick(() => {
      window.history.length > 1 ? navigate(-1) : navigate(fallbackPath);
    });

    return () => {
      offClick();
      backButton.hide();
    };
  }, [pathname, navigate, fallbackPath]);
}

Edge cases:

  • Deep linkshistory.length may be 1 on first open. Always provide a fallback route.
  • Modals — BackButton should close the top overlay before navigating away.
  • Root screen — hide BackButton; users dismiss the Mini App via Telegram’s own chrome.

Multi-Step Flows: Combining Both Buttons

Wizards (cart → delivery → payment) are where native buttons shine. MainButton advances or submits; BackButton walks back through steps or exits to home. Keep step state in React, update button labels in useEffect, and reset both buttons on unmount.

For long operations, set isLoaderVisible: true and isEnabled: false before the API call — prevents double-submit better than debounce alone.

Fullscreen, Safe Area, and Haptics

In fullscreen mode (Bot API 7.7+), expand the viewport and respect CSS variables:

.page {
  padding-top: var(--tg-content-safe-area-inset-top, 0px);
  padding-bottom: calc(var(--tg-content-safe-area-inset-bottom, 0px) + 72px);
}

Optional haptic feedback on success:

import { hapticFeedback } from '@telegram-apps/sdk-react';

hapticFeedback.notificationOccurred('success');

Use haptics sparingly — light impact on MainButton tap is enough for most forms.

Common Mistakes

  • MainButton never showsinit() missing, or app opened outside Telegram.
  • Double submit — forgot loader/disabled state during fetch.
  • StrictMode double handlers in dev — fix with proper cleanup; production is fine.
  • BackButton closes app — empty history; use fallback navigation.
  • Hardcoded colors — use theme CSS variables instead.

Extract reusable hooks early: useTelegramBackButton for layout, useMainButton({ text, enabled, onClick }) for forms. Your route components stay focused on business logic.

Need help shipping your Mini App? Telegram → or vic.kell@ya.ru

FAQ

Can I customize MainButton color?
Not directly — it follows the bot theme. Configure theme in BotFather, or use a custom HTML button in fullscreen (you lose native UX).

Does MainButton work in a regular browser?
No. WebApp APIs exist only inside Telegram. Use SDK mocks or test via t.me links.

MainButton vs inline bot buttons?
Different contexts. Inline buttons live in chat; MainButton lives inside the WebApp. For in-app checkout, MainButton is the right choice.

Is @twa-dev/sdk still supported?
It’s in maintenance. Start new projects on @telegram-apps/sdk-react.

Conclusion

MainButton and BackButton are the foundation of native-feeling TWA navigation. Primary action at the bottom, back in the header — users shouldn’t hunt for controls. With React + Vite, it’s a couple of hooks, disciplined cleanup, and syncing button state with your forms and router.

Get the layout hook in place first, then wire MainButton per screen. Skip duplicate HTML CTAs, respect safe areas in fullscreen, and your Mini App will feel like part of Telegram — not a webpage squeezed into a WebView.

KEL IT

Need a custom solution?

I build these types of projects professionally. Telegram bots, Mini Apps, websites, mobile and desktop applications. Tell me about your project and I'll get back to you with a plan.