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