Haptic Feedback и темы оформления в Telegram Mini App
Если вы уже разобрались с авторизацией через initData и настроили MainButton / BackButton, следующий шаг — сделать приложение по-настоящему нативным. Два инструмента, которые мгновенно поднимают UX: haptic feedback (тактильная отдача) и автоматическая адаптация темы под системное оформление Telegram.
На первый взгляд кажется мелочью. На практике — пользователи замечают разницу. Приложение с вибрацией на кнопку «Оформить заказ» и с правильными цветами в тёмной теме воспринимается как продуманный продукт, а не как сайт, вставленный в WebView.
В этой статье разберём:
- как работает
tg.HapticFeedbackи когда его применять; - как читать переменные темы из
tg.themeParams; - как реагировать на смену темы в реальном времени;
- как обернуть всё это в удобные хуки для React.
Как устроен Haptic Feedback в Telegram WebApp
Telegram WebApp SDK предоставляет объект window.Telegram.WebApp.HapticFeedback с тремя методами:
// Ударная вибрация — для кнопок, подтверждений
impactOccurred(style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft')
// Уведомление — успех, ошибка, предупреждение
notificationOccurred(type: 'error' | 'success' | 'warning')
// Выбор изменился — для слайдеров, списков
selectionChanged()
Важный нюанс: вибрация работает только на мобильных устройствах (iOS и Android). На десктопе методы вызываются без ошибок, просто ничего не происходит. Проверять платформу вручную не нужно — SDK сам обрабатывает этот случай.
Создаём хук useHaptic
Чтобы не тащить window.Telegram.WebApp в каждый компонент, создадим тонкую обёртку:
// src/hooks/useHaptic.ts
import { useCallback } from 'react';
type ImpactStyle = 'light' | 'medium' | 'heavy' | 'rigid' | 'soft';
type NotificationType = 'error' | 'success' | 'warning';
export function useHaptic() {
const tg = window.Telegram?.WebApp;
const impact = useCallback(
(style: ImpactStyle = 'medium') => {
tg?.HapticFeedback?.impactOccurred(style);
},
[tg]
);
const notify = useCallback(
(type: NotificationType) => {
tg?.HapticFeedback?.notificationOccurred(type);
},
[tg]
);
const selection = useCallback(() => {
tg?.HapticFeedback?.selectionChanged();
}, [tg]);
return { impact, notify, selection };
}
Применяем в компонентах
// src/components/OrderButton.tsx
import { useHaptic } from '../hooks/useHaptic';
export function OrderButton({ onOrder }: { onOrder: () => void }) {
const { impact, notify } = useHaptic();
const handleClick = async () => {
impact('medium'); // тактильный отклик при нажатии
try {
await onOrder();
notify('success'); // успешное оформление заказа
} catch {
notify('error'); // что-то пошло не так
}
};
return (
<button onClick={handleClick} className="order-btn">
Оформить заказ
</button>
);
}
Рекомендации по использованию:
impactOccurred('light')— мелкие действия: тоггл, чекбокс;impactOccurred('medium')— основные CTA-кнопки;impactOccurred('heavy')— деструктивные действия (удалить, отменить);notificationOccurred('success')— завершение операции, оплата;notificationOccurred('error')— ошибка валидации, сбой запроса;selectionChanged()— прокрутка списка, выбор категории.
Не ставьте вибрацию на каждый чих. Если пользователь скроллит список и selectionChanged() срабатывает на каждый пиксель — это раздражает. Применяйте haptic только на осмысленные события.
Темы оформления: читаем tg.themeParams
Telegram передаёт в Mini App набор CSS-переменных и объект themeParams с текущей цветовой схемой. Это позволяет вашему приложению выглядеть органично — как в светлой, так и в тёмной теме.
const tg = window.Telegram.WebApp;
console.log(tg.themeParams);
// {
// bg_color: "#ffffff",
// text_color: "#000000",
// hint_color: "#999999",
// link_color: "#2678b6",
// button_color: "#2678b6",
// button_text_color: "#ffffff",
// secondary_bg_color: "#f1f1f1",
// header_bg_color: "#ffffff",
// accent_text_color: "#1c93e3",
// section_bg_color: "#ffffff",
// section_header_text_color: "#6d6d71",
// subtitle_text_color: "#999999",
// destructive_text_color: "#ff3b30"
// }
Telegram также автоматически инжектирует CSS-переменные в document.documentElement. Это значит, что вы можете использовать их напрямую в стилях:
.card {
background-color: var(--tg-theme-bg-color);
color: var(--tg-theme-text-color);
border: 1px solid var(--tg-theme-hint-color);
}
.btn-primary {
background-color: var(--tg-theme-button-color);
color: var(--tg-theme-button-text-color);
}
.secondary {
background-color: var(--tg-theme-secondary-bg-color);
}
Это самый простой и надёжный способ — никаких JavaScript-хуков, просто CSS.
Если нужна подобная разработка под ключ — напишите в Telegram.
Хук useTheme: реактивная адаптация темы
CSS-переменных достаточно для большинства задач. Но иногда нужно знать текущую тему в JavaScript — например, чтобы передать цвет в canvas, SVG или стороннюю библиотеку графиков.
Telegram WebApp генерирует событие themeChanged при смене темы (например, когда пользователь меняет системную тему на лету):
// src/hooks/useTheme.ts
import { useState, useEffect } from 'react';
type ThemeParams = {
bg_color: string;
text_color: string;
hint_color: string;
link_color: string;
button_color: string;
button_text_color: string;
secondary_bg_color: string;
[key: string]: string;
};
export function useTheme() {
const tg = window.Telegram?.WebApp;
const [theme, setTheme] = useState<ThemeParams>(
(tg?.themeParams as ThemeParams) ?? {}
);
const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(
tg?.colorScheme ?? 'light'
);
useEffect(() => {
if (!tg) return;
const handleThemeChange = () => {
setTheme(tg.themeParams as ThemeParams);
setColorScheme(tg.colorScheme);
};
tg.onEvent('themeChanged', handleThemeChange);
return () => tg.offEvent('themeChanged', handleThemeChange);
}, [tg]);
return { theme, colorScheme, isDark: colorScheme === 'dark' };
}
Использование в компоненте с Chart.js
import { useTheme } from '../hooks/useTheme';
import { Line } from 'react-chartjs-2';
export function RevenueChart({ data }: { data: number[] }) {
const { theme, isDark } = useTheme();
const chartData = {
labels: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'],
datasets: [
{
label: 'Выручка',
data,
borderColor: theme.button_color ?? '#2678b6',
backgroundColor: isDark
? 'rgba(38, 120, 182, 0.1)'
: 'rgba(38, 120, 182, 0.2)',
},
],
};
const options = {
plugins: {
legend: {
labels: { color: theme.text_color ?? '#000000' },
},
},
scales: {
x: { ticks: { color: theme.hint_color ?? '#999999' } },
y: { ticks: { color: theme.hint_color ?? '#999999' } },
},
};
return <Line data={chartData} options={options} />;
}
Настройка headerColor и backgroundColor
Помимо темы контента, можно управлять цветом шапки Telegram и фоном самого WebView:
const tg = window.Telegram.WebApp;
// Цвет заголовка (статус-бар и хедер Telegram)
// 'bg_color' | 'secondary_bg_color' | '#RRGGBB'
tg.setHeaderColor('bg_color');
// Цвет фона под WebView (виден при резиновой прокрутке)
tg.setBackgroundColor(tg.themeParams.bg_color ?? '#ffffff');
Это особенно важно при использовании fullscreen mode (tg.requestFullscreen()). Когда приложение разворачивается на весь экран, фоновый цвет Telegram становится виден за границами вашего контента. Если не задать его явно — получите белый прямоугольник в тёмной теме.
// src/main.tsx — инициализация приложения
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const tg = window.Telegram?.WebApp;
if (tg) {
tg.ready();
tg.expand(); // разворачиваем на весь экран
tg.setHeaderColor('bg_color');
tg.setBackgroundColor(tg.themeParams.bg_color ?? '#ffffff');
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Собираем всё вместе: TelegramProvider
Удобнее всего вынести инициализацию и все данные из SDK в один контекст:
// src/providers/TelegramProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
type TelegramContextType = {
tg: typeof window.Telegram.WebApp | null;
isDark: boolean;
theme: Record<string, string>;
haptic: {
impact: (style?: 'light' | 'medium' | 'heavy') => void;
notify: (type: 'success' | 'error' | 'warning') => void;
};
};
const TelegramContext = createContext<TelegramContextType | null>(null);
export function TelegramProvider({ children }: { children: React.ReactNode }) {
const tg = window.Telegram?.WebApp ?? null;
const [isDark, setIsDark] = useState(tg?.colorScheme === 'dark');
const [theme, setTheme] = useState<Record<string, string>>(
(tg?.themeParams as Record<string, string>) ?? {}
);
useEffect(() => {
if (!tg) return;
tg.ready();
tg.expand();
tg.setHeaderColor('bg_color');
tg.setBackgroundColor(tg.themeParams.bg_color ?? '#ffffff');
const onChange = () => {
setIsDark(tg.colorScheme === 'dark');
setTheme(tg.themeParams as Record<string, string>);
tg.setBackgroundColor(tg.themeParams.bg_color ?? '#ffffff');
};
tg.onEvent('themeChanged', onChange);
return () => tg.offEvent('themeChanged', onChange);
}, [tg]);
const haptic = {
impact: (style: 'light' | 'medium' | 'heavy' = 'medium') =>
tg?.HapticFeedback?.impactOccurred(style),
notify: (type: 'success' | 'error' | 'warning') =>
tg?.HapticFeedback?.notificationOccurred(type),
};
return (
<TelegramContext.Provider value={{ tg, isDark, theme, haptic }}>
{children}
</TelegramContext.Provider>
);
}
export function useTelegram() {
const ctx = useContext(TelegramContext);
if (!ctx) throw new Error('useTelegram must be used inside TelegramProvider');
return ctx;
}
Оборачиваем приложение:
// src/main.tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
<TelegramProvider>
<App />
</TelegramProvider>
);
Теперь в любом компоненте:
function PayButton() {
const { haptic } = useTelegram();
return (
<button
onClick={() => {
haptic.impact('heavy');
// логика оплаты
}}
>
Оплатить
</button>
);
}
Типизация SDK через @types/telegram-web-app
Чтобы не писать window.Telegram.WebApp с типом any, установите пакет с типами:
npm install -D @types/telegram-web-app
Добавьте в tsconfig.json:
{
"compilerOptions": {
"types": ["telegram-web-app"]
}
}
Теперь window.Telegram.WebApp полностью типизирован — автодополнение, проверка аргументов и никаких // @ts-ignore.
Нужна помощь? Написать в Telegram → или vic.kell@ya.ru
FAQ
Haptic feedback не работает в iOS Telegram. Почему?
Убедитесь, что вы вызываете tg.ready() перед любыми обращениями к SDK. Также проверьте версию Telegram — haptic доступен начиная с Bot API 6.1. На старых клиентах методы существуют, но ничего не делают.
Как узнать, что тема изменилась в реальном времени?
Используйте tg.onEvent('themeChanged', callback). Событие срабатывает мгновенно, когда пользователь меняет тему в системных настройках телефона (при открытом Mini App).
Можно ли использовать собственные цвета вместо цветов темы Telegram?
Технически — да. Практически — нежелательно для основных элементов интерфейса. Пользователь ожидает, что приложение внутри Telegram следует общей цветовой схеме. Используйте свои цвета только для брендовых акцентов, фоны и текст лучше брать из themeParams.
Что происходит, если приложение открыто не в Telegram (например, в браузере при разработке)?
window.Telegram будет undefined. Все обращения через опциональную цепочку (tg?.HapticFeedback?.impactOccurred) просто ничего не сделают. Для удобной разработки вне Telegram можно использовать пакет @telegram-apps/sdk с mock-режимом.
Нужно ли вручную применять CSS-переменные темы или они проставляются сами?
Telegram автоматически инжектирует переменные вида --tg-theme-bg-color, --tg-theme-text-color и т.д. в document.documentElement при запуске Mini App. Вам достаточно использовать их в CSS — никакого JavaScript для базовой стилизации не нужно.
Заключение
Haptic feedback и адаптация темы — небольшой объём кода, но ощутимый прирост в качестве продукта. Три кастомных хука (useHaptic, useTheme, useTelegram) покрывают 95% реальных сценариев.
Главные принципы:
- Не злоупотребляйте вибрацией — только на значимые события;
- Используйте CSS-переменные темы как первый выбор, JavaScript — для сложных случаев;
- Обрабатывайте отсутствие
window.Telegram— приложение должно запускаться и в браузере при разработке; - Вызывайте
tg.setBackgroundColor()при инициализации и при каждой смене темы.
Следующий логичный шаг — biometric auth: tg.BiometricManager позволяет аутентифицировать пользователя по отпечатку пальца или Face ID прямо внутри Mini App. Разберём в следующей статье.