Haptic Feedback & Themes in Telegram Mini Apps — KEL IT
Telegram Mini Apps 6 min read

Haptic Feedback & Dynamic Theming in Telegram Mini Apps

Building a Telegram Mini App that feels truly native means going beyond basic functionality. Two features that make the biggest difference in perceived quality: haptic feedback and automatic theme adaptation. Together they turn a generic WebView into something that feels like it belongs in Telegram.

This guide covers the practical implementation using React + Vite, with reusable hooks you can drop into any project.

Understanding tg.HapticFeedback

The Telegram WebApp SDK exposes three haptic methods via window.Telegram.WebApp.HapticFeedback:

// Physical button presses, confirmations
impactOccurred(style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft')

// Operation outcomes
notificationOccurred(type: 'error' | 'success' | 'warning')

// Selection changes in lists or pickers
selectionChanged()

Haptic only fires on real mobile devices — iOS and Android. On desktop the calls are silently ignored, so no platform checks needed.

A minimal useHaptic hook

// src/hooks/useHaptic.ts
import { useCallback } from 'react';

export function useHaptic() {
  const tg = window.Telegram?.WebApp;

  const impact = useCallback(
    (style: 'light' | 'medium' | 'heavy' = 'medium') => {
      tg?.HapticFeedback?.impactOccurred(style);
    },
    [tg]
  );

  const notify = useCallback(
    (type: 'error' | 'success' | 'warning') => {
      tg?.HapticFeedback?.notificationOccurred(type);
    },
    [tg]
  );

  const selection = useCallback(() => {
    tg?.HapticFeedback?.selectionChanged();
  }, [tg]);

  return { impact, notify, selection };
}

When to use which method:

  • impact('light') — toggles, checkboxes, minor interactions
  • impact('medium') — primary action buttons (Add to cart, Confirm)
  • impact('heavy') — destructive actions (Delete, Cancel order)
  • notify('success') — payment complete, form submitted
  • notify('error') — validation failure, network error
  • selection() — scrollable pickers, tab switches

Avoid overuse. If every scroll tick triggers selectionChanged(), it becomes annoying fast. Reserve haptics for intentional, meaningful user actions.

Theme Adaptation with themeParams

Telegram automatically injects CSS custom properties into document.documentElement when your Mini App launches:

/* These work out of the box — no JavaScript needed */
.card {
  background: var(--tg-theme-bg-color);
  color: var(--tg-theme-text-color);
}

.button {
  background: var(--tg-theme-button-color);
  color: var(--tg-theme-button-text-color);
}

.hint {
  color: var(--tg-theme-hint-color);
}

.secondary-panel {
  background: var(--tg-theme-secondary-bg-color);
}

For most use cases, CSS variables are all you need. They update automatically when the user switches themes.

Reactive Theme Hook for JS-based Styling

When you need theme values in JavaScript — for canvas, SVG, or third-party chart libraries — use the themeChanged event:

// src/hooks/useTheme.ts
import { useState, useEffect } from 'react';

export function useTheme() {
  const tg = window.Telegram?.WebApp;

  const [isDark, setIsDark] = useState(tg?.colorScheme === 'dark');
  const [theme, setTheme] = useState(
    (tg?.themeParams as Record<string, string>) ?? {}
  );

  useEffect(() => {
    if (!tg) return;
    const handler = () => {
      setIsDark(tg.colorScheme === 'dark');
      setTheme(tg.themeParams as Record<string, string>);
    };
    tg.onEvent('themeChanged', handler);
    return () => tg.offEvent('themeChanged', handler);
  }, [tg]);

  return { theme, isDark };
}

Practical example with a chart library:

import { useTheme } from '../hooks/useTheme';

export function SalesChart({ data }: { data: number[] }) {
  const { theme, isDark } = useTheme();

  // Pass Telegram theme colors to the chart config
  const lineColor = theme.button_color ?? '#2678b6';
  const textColor = theme.text_color ?? '#000000';
  const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';

  // ... chart configuration using these values
  return <div>/* chart here */</div>;
}

Header and Background Colors

When using tg.expand() or fullscreen mode, the Telegram UI chrome becomes visible. Match it to your app:

// src/main.tsx
const tg = window.Telegram?.WebApp;
if (tg) {
  tg.ready();
  tg.expand();
  // Match Telegram header to your app background
  tg.setHeaderColor('bg_color');
  // Prevent white flash on rubber-band scroll
  tg.setBackgroundColor(tg.themeParams.bg_color ?? '#ffffff');
}

Always update setBackgroundColor inside the themeChanged handler too — otherwise you’ll get a mismatched background when the user switches themes with the app open.

A Unified TelegramProvider

For larger apps, centralise everything in one context:

// src/providers/TelegramProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';

type Ctx = {
  isDark: boolean;
  theme: Record<string, string>;
  haptic: {
    impact: (s?: 'light' | 'medium' | 'heavy') => void;
    notify: (t: 'success' | 'error' | 'warning') => void;
  };
};

const Ctx = createContext<Ctx | 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 sync = () => {
      setIsDark(tg.colorScheme === 'dark');
      setTheme(tg.themeParams as Record<string, string>);
      tg.setBackgroundColor(tg.themeParams.bg_color ?? '#ffffff');
    };
    tg.onEvent('themeChanged', sync);
    return () => tg.offEvent('themeChanged', sync);
  }, [tg]);

  const haptic = {
    impact: (s: 'light' | 'medium' | 'heavy' = 'medium') =>
      tg?.HapticFeedback?.impactOccurred(s),
    notify: (t: 'success' | 'error' | 'warning') =>
      tg?.HapticFeedback?.notificationOccurred(t),
  };

  return <Ctx.Provider value={{ isDark, theme, haptic }}>{children}</Ctx.Provider>;
}

export const useTelegram = () => {
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error('useTelegram outside TelegramProvider');
  return ctx;
};

Add TypeScript types for the SDK:

npm install -D @types/telegram-web-app

Then in tsconfig.json:

{ "compilerOptions": { "types": ["telegram-web-app"] } }

FAQ

Haptic feedback isn’t working on iOS. What’s wrong?

Make sure tg.ready() is called before any SDK interactions. Also verify the user’s Telegram client supports Bot API 6.1 or later — older versions have the methods but they’re no-ops.

Do I need JavaScript to apply Telegram theme colors?

No. For standard UI elements, CSS variables (var(--tg-theme-bg-color) etc.) are injected automatically and update on theme change. Use JavaScript only when you need theme values in non-CSS contexts like canvas or chart configs.

How do I develop locally without Telegram?

window.Telegram will be undefined. Use optional chaining (tg?.HapticFeedback?.impactOccurred(...)) throughout. For a better dev experience, check out @telegram-apps/sdk which includes a mock environment for local development.

Should I use tg.themeParams or CSS variables?

Prefer CSS variables for UI components — they’re simpler and automatically reactive. Use tg.themeParams via the useTheme hook when you need colors in JavaScript logic, event handlers, or third-party integrations.

Wrapping Up

Haptic feedback and theme adaptation are low-effort, high-impact improvements. A Mini App that vibrates correctly on key actions and looks right in both light and dark themes signals craftsmanship to users.

Core takeaways:

  • Keep haptics intentional — major actions only
  • CSS variables handle 95% of theming needs
  • Always sync setBackgroundColor with the theme
  • Wrap everything in a TelegramProvider for clean, scalable architecture

The next step in native-feeling Mini Apps: biometric authentication via tg.BiometricManager — fingerprint and Face ID support built directly into the SDK.

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.