MainButton и BackButton в Telegram Mini App — KEL IT
Telegram Mini Apps 9 мин чтения

MainButton и BackButton в Telegram Mini App: нативная навигация на React + Vite

Пользователь открывает ваш Mini App внутри Telegram и ожидает поведение «как в приложении»: крупная кнопка действия внизу экрана, стрелка «назад» в шапке клиента, без лишних HTML-кнопок, которые выглядят чужеродно на фоне интерфейса мессенджера. Именно для этого Telegram предоставляет MainButton и BackButton — нативные элементы WebApp SDK, которые рендерятся клиентом Telegram, а не вашим HTML.

В 2026 году рекомендуемый стек для React-проектов — @telegram-apps/sdk-react. Он оборачивает window.Telegram.WebApp, даёт реактивные хуки и избавляет от ручной подписки на события. В этой статье разберём production-паттерны: когда показывать кнопки, как связать их с React Router, как обрабатывать загрузку и ошибки, и почему дублирование MainButton собственной кнопкой «Купить» внизу страницы — плохая идея.

Зачем нужны MainButton и BackButton

MainButton (tg.MainButton) — фиксированная кнопка в нижней части экрана Telegram. Её видят все пользователи Mini Apps: она используется для оформления заказа, подтверждения формы, оплаты, отправки заявки. Цвет кнопки автоматически берётся из темы бота (themeParams.button_color), текст — настраивается вами.

BackButton (tg.BackButton) — стрелка «назад» слева в шапке WebView. Она не занимает место в вашей вёрстке и не конфликтует с системной навигацией Android/iOS. На корневом экране её обычно скрывают; на вложенных — показывают.

Преимущества перед обычными <button>:

  • Единый UX — пользователь уже привык к MainButton в других Mini Apps (магазины, игры, сервисы доставки).
  • Безопасная зона — Telegram сам учитывает safe area и высоту нижней панели; вам не нужно подбирать padding-bottom под каждый девайс.
  • Haptic feedback — при нажатии клиент может проиграть тактильный отклик (на поддерживаемых платформах).
  • Меньше «веба» — приложение ощущается нативным, а не сайтом в iframe.

Подключение SDK и базовая инициализация

Создайте проект и установите официальный SDK:

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

Инициализация в main.tsx:

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

init(); // монтирует Telegram WebApp API

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(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

Для локальной разработки без Telegram подключите mock. Пакет @telegram-apps/sdk умеет эмулировать WebApp в браузере, если передать launchParams через query string или использовать dev-tools. Главное правило: не вызывайте методы MainButton до init() — иначе получите undefined is not a function.

Минимальный пример — показать MainButton на главной:

// src/pages/Home.tsx
import { useEffect } from 'react';
import { mainButton } from '@telegram-apps/sdk-react';

export function Home() {
  useEffect(() => {
    mainButton.setParams({
      text: 'Перейти к оформлению',
      isVisible: true,
      isEnabled: true,
    });

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

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

  return <h1>Каталог</h1>;
}

Обратите внимание на cleanup в return: при размонтировании компонента скрывайте кнопку и отписывайтесь от onClick. Без этого при переходе между страницами останутся «висящие» обработчики, и одно нажатие вызовет несколько колбэков.

Если нужна подобная разработка — напишите в Telegram.

MainButton: состояния, текст и UX-паттерны

MainButton поддерживает несколько состояний, которые нужно явно управлять:

СостояниеМетод / параметрКогда использовать
СкрытаmainButton.hide()Форма не заполнена, экран только для чтения
Видима, активнаisEnabled: trueВсе поля валидны, можно отправить
Видима, неактивнаisEnabled: falseНе хватает данных, идёт валидация
ЗагрузкаisLoaderVisible: trueЗапрос на сервер, блокируем повторный клик
ПрогрессshowProgress(leaveActive)Длительная операция (загрузка файла)

Пример формы с динамическим состоянием:

// src/pages/Checkout.tsx
import { useEffect } from 'react';
import { mainButton } from '@telegram-apps/sdk-react';
import { useForm } from 'react-hook-form';

type FormData = { address: string; phone: string };

export function Checkout() {
  const { register, handleSubmit, formState: { isValid, isSubmitting } } =
    useForm<FormData>({ mode: 'onChange' });

  useEffect(() => {
    mainButton.setParams({
      text: isSubmitting ? 'Отправляем…' : 'Подтвердить заказ',
      isVisible: true,
      isEnabled: isValid && !isSubmitting,
      isLoaderVisible: isSubmitting,
    });
  }, [isValid, isSubmitting]);

  useEffect(() => {
    const offClick = mainButton.onClick(() => {
      handleSubmit(onSubmit)();
    });
    return () => offClick();
  }, [handleSubmit]);

  async function onSubmit(data: FormData) {
    await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify(data),
    });
    mainButton.hide();
  }

  return (
    <form>
      <input {...register('address', { required: true })} placeholder="Адрес" />
      <input {...register('phone', { required: true })} placeholder="Телефон" />
    </form>
  );
}

Паттерны UX:

  1. Один MainButton на экран — не пытайтесь показать две разные MainButton; у WebApp она одна. Вторary action разместите в контенте (ссылка «Пропустить», текстовая кнопка).
  2. Текст кнопки = глагол + объект — «Оплатить 1 990 ₽» лучше, чем «Далее». Telegram ограничивает длину текста; на узких экранах длинные строки обрезаются.
  3. Не дублируйте MainButton HTML-кнопкой — две одинаковые кнопки «Оформить» внизу страницы сбивают пользователя. Либо MainButton, либо своя sticky-кнопка (если нужен кастомный дизайн в fullscreen-режиме).
  4. Скрывайте на экранах без действия — список товаров, профиль, FAQ не нуждаются в MainButton. Пустая или бессмысленная кнопка выглядит как баг.

BackButton и многостраничная навигация

BackButton решает проблему «как вернуться назад без своего header». На Android Telegram уже перехватывает системную кнопку «назад», но на iOS и Desktop поведение зависит от вашей логики.

Базовый паттерн с React Router:

// src/hooks/useTelegramBackButton.ts
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 location = useLocation();

  useEffect(() => {
    const isRoot = location.pathname === fallbackPath;

    if (isRoot) {
      backButton.hide();
      return;
    }

    backButton.show();

    const offClick = backButton.onClick(() => {
      if (window.history.length > 1) {
        navigate(-1);
      } else {
        navigate(fallbackPath);
      }
    });

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

Подключение в layout:

// src/layouts/AppLayout.tsx
import { Outlet } from 'react-router-dom';
import { useTelegramBackButton } from '../hooks/useTelegramBackButton';

export function AppLayout() {
  useTelegramBackButton('/');

  return (
    <main style={{ paddingBottom: 'var(--tg-content-safe-area-inset-bottom, 80px)' }}>
      <Outlet />
    </main>
  );
}

Важные нюансы:

  • На корневом экране BackButton скрыта — пользователь закрывает Mini App свайпом вниз или через «×» в шапке Telegram.
  • При модальных окнах (drawer, bottom sheet) BackButton может закрывать модалку, а не уходить на предыдущий роут. Заведите стек overlay-состояний и обрабатывайте BackButton в приоритете модалки.
  • Deep link (t.me/bot/app?startapp=product_42) — при первом открытии history.length может быть 1. Fallback на / или /catalog обязателен.

Связка MainButton + BackButton на wizard-флоу

Типичный сценарий — многошаговое оформление: «Корзина → Доставка → Оплата». MainButton меняет текст на каждом шаге, BackButton возвращает на предыдущий шаг.

// src/pages/OrderWizard.tsx
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { mainButton, backButton } from '@telegram-apps/sdk-react';

const STEPS = ['cart', 'delivery', 'payment'] as const;

export function OrderWizard() {
  const [stepIndex, setStepIndex] = useState(0);
  const navigate = useNavigate();
  const step = STEPS[stepIndex];

  const labels: Record<typeof step, string> = {
    cart: 'К доставке',
    delivery: 'К оплате',
    payment: 'Оплатить',
  };

  useEffect(() => {
    backButton.show();
    const offBack = backButton.onClick(() => {
      if (stepIndex > 0) setStepIndex((i) => i - 1);
      else navigate('/');
    });

    mainButton.setParams({ text: labels[step], isVisible: true, isEnabled: true });
    const offMain = mainButton.onClick(() => {
      if (stepIndex < STEPS.length - 1) setStepIndex((i) => i + 1);
      else submitPayment();
    });

    return () => {
      offBack();
      offMain();
      backButton.hide();
      mainButton.hide();
    };
  }, [stepIndex, step, navigate]);

  return <div>Шаг: {step}</div>;
}

async function submitPayment() {
  mainButton.setParams({ isLoaderVisible: true, isEnabled: false });
  // …
}

Такой подход избавляет от лишних «Далее» / «Назад» в разметке и держит фокус пользователя на контенте.

Fullscreen, safe area и haptic feedback

С Bot API 7.7+ Mini Apps поддерживают fullscreen mode — контент занимает весь экран, MainButton остаётся в зоне Telegram. При переходе в fullscreen проверьте отступы:

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

useEffect(() => {
  if (viewport.expand.isAvailable()) {
    viewport.expand();
  }
}, []);

CSS-переменные для safe area:

.page {
  padding-top: var(--tg-content-safe-area-inset-top, 0px);
  padding-bottom: calc(
    var(--tg-content-safe-area-inset-bottom, 0px) + 72px
  ); /* место под MainButton */
}

Haptic feedback при успешном действии:

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

function onSuccess() {
  if (hapticFeedback.notificationOccurred.isAvailable()) {
    hapticFeedback.notificationOccurred('success');
  }
}

Лёгкий impact при нажатии MainButton (опционально, не злоупотребляйте):

mainButton.onClick(() => {
  hapticFeedback.impactOccurred('light');
  handleSubmit();
});

Типичные ошибки и отладка

«MainButton не появляется»

Проверьте: вызван ли init(), открыто ли приложение именно как Mini App (не просто ссылка в браузере), установлен ли isVisible: true. В Desktop-клиенте Telegram MainButton иногда рендерится уже — обновите Telegram до актуальной версии.

Двойной клик и повторная отправка формы

Не забывайте isLoaderVisible: true и isEnabled: false на время запроса. Debounce на onClick — запасной вариант, но блокировка через состояние кнопки надёжнее.

StrictMode и двойные подписки

В React 18 StrictMode эффекты монтируются дважды в dev. Cleanup с offClick() обязателен. Если видите двойной вызов только в dev — это нормально; в production будет один вызов.

BackButton конфликтует с модалкой

Решение: глобальный NavigationStack — сначала закрываем верхний overlay, потом navigate(-1).

Hardcoded цвета

Не задавайте цвет MainButton вручную — Telegram использует themeParams.button_color. Для контента применяйте var(--tg-theme-text-color) и var(--tg-theme-bg-color).

Нужна помощь? Telegram → или vic.kell@ya.ru

FAQ

Можно ли изменить цвет MainButton?
Напрямую — нет, цвет берётся из темы бота. Косвенно — через @BotFather → Bot Settings → Configure Mini App → Theme. Для полностью кастомной кнопки используйте HTML в fullscreen-режиме, но вы потеряете нативный UX.

MainButton работает в обычном браузере?
Нет. Вне Telegram WebApp API недоступен. Для dev используйте mock SDK или тестовый запуск через t.me-ссылку.

Сколько символов влезает в текст MainButton?
Официального лимита в документации нет, но на практике держите текст до 25–30 символов. Длинные суммы форматируйте компактно: «Оплатить 1,9K ₽».

BackButton закрывает Mini App вместо navigate(-1)?
Так бывает, если history пуст. Добавьте fallback-роут. На Android системная кнопка «назад» может закрыть WebView — обработайте viewport closing confirmation, если нужно предупредить о несохранённых данных.

Нужен ли MainButton, если есть inline-кнопки бота?
Да, это разные контексты. Inline-кнопки работают в чате; MainButton — внутри WebApp. Для checkout и форм MainButton удобнее: пользователь уже в приложении, не нужно возвращаться в переписку.

@twa-dev/sdk ещё актуален?
В 2026 году для новых проектов используйте @telegram-apps/sdk-react. Старый пакет @twa-dev/sdk в maintenance mode; API MainButton/BackButton совместим, но хуки и tree-shaking лучше в новом SDK.

Заключение

MainButton и BackButton — базовый слой нативного UX в Telegram Mini App. Пользователь не должен искать «куда нажать»: действие — в MainButton, возврат — через BackButton. На React + Vite это сводится к паре хуков с правильным cleanup, синхронизации состояния формы и роутера.

Начните с layout-хука useTelegramBackButton, вынесите управление MainButton в отдельный хук useMainButton({ text, enabled, onClick }), и не дублируйте нативные элементы HTML-кнопками. Так Mini App будет ощущаться частью Telegram, а не вкладкой браузера — без лишней вёрстки и с меньшим количеством багов на iOS и Android.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.