Haptic Feedback и темы в Telegram Mini App — KEL IT
Telegram Mini Apps 8 мин чтения

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. Разберём в следующей статье.

KEL IT

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

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