skills/telegram-mini-apps-react/SKILL.md
Comprehensive guide for creating Telegram Mini Apps with React using @tma.js/sdk-react. Covers SDK initialization, component mounting, signals, theming, back button handling, viewport management, init data, deep linking, and environment mocking for development. Use when building or debugging Telegram Mini Apps with React.
npx skillsauth add nailorsh/agents_utils telegram-mini-apps-reactInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides comprehensive guidance for building Telegram Mini Apps using React and the @tma.js/sdk-react package.
Telegram Mini Apps are web applications displayed inside Telegram's WebView. They integrate with Telegram's native UI components (Back Button, Main Button) and have access to user data, theme parameters, and platform-specific features.
Key concepts:
# For React projects, install the React-specific package
pnpm i @tma.js/sdk-react
# DO NOT install both @tma.js/sdk and @tma.js/sdk-react - this causes bugs!
Important: The
@tma.js/sdk-reactpackage fully re-exports@tma.js/sdk, so you don't need to install them separately.
pnpm dlx @tma.js/create-mini-app@latest
# or
npx @tma.js/create-mini-app@latest
This CLI scaffolds a complete project with proper configuration.
A typical Telegram Mini App React project structure:
src/
├── main.tsx # Entry point - SDK initialization
├── init.ts # SDK configuration and component mounting
├── mockEnv.ts # Development environment mocking
├── App.tsx # Main React app with routing
├── components/
│ ├── Page.tsx # Page wrapper with back button handling
│ └── EnvUnsupported.tsx # Fallback for non-TG environments
├── hooks/
│ └── useDeeplink.ts # Deep linking handler
└── services/
└── analytics.ts # Analytics with user data
The SDK must be initialized before using any features. See references/init.md for detailed implementation.
import {
init as initSDK,
setDebug,
themeParams,
miniApp,
viewport,
backButton,
swipeBehavior,
initData
} from '@tma.js/sdk-react';
export async function init(options: {
debug: boolean;
eruda: boolean;
mockForMacOS: boolean;
}): Promise<void> {
// Enable debug mode for development
setDebug(options.debug);
// Initialize the SDK (REQUIRED before using any features)
initSDK();
// Mount components you'll use in the app
backButton.mount.ifAvailable();
initData.restore();
// Configure swipe behavior
if (swipeBehavior.isSupported()) {
swipeBehavior.mount();
swipeBehavior.disableVertical();
}
// Setup Mini App theming
if (miniApp.mount.isAvailable()) {
themeParams.mount();
miniApp.mount();
themeParams.bindCssVars(); // Binds theme to CSS variables
}
// Configure viewport
if (viewport.mount.isAvailable()) {
viewport.mount().then(() => {
viewport.bindCssVars();
viewport.requestFullscreen();
});
}
}
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { retrieveLaunchParams } from '@tma.js/sdk-react';
import { init } from './init';
import App from "./App";
import { EnvUnsupported } from "./components/EnvUnsupported";
// Mock environment for local development
import './mockEnv';
const root = ReactDOM.createRoot(document.getElementById('root')!);
try {
const launchParams = retrieveLaunchParams();
const { tgWebAppPlatform: platform } = launchParams;
const debug = (launchParams.tgWebAppStartParam || '').includes('debug')
|| import.meta.env.DEV;
await init({
debug,
eruda: debug && ['ios', 'android'].includes(platform),
mockForMacOS: platform === 'macos',
}).then(() => {
root.render(
<StrictMode>
<App/>
</StrictMode>,
);
});
} catch (e) {
// Show fallback UI when not in Telegram
root.render(<EnvUnsupported/>);
}
The Back Button is a native Telegram UI element that appears in the header.
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { backButton, miniApp } from '@tma.js/sdk-react';
export function Page({ children, back = true }) {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (back) {
backButton.show();
// onClick returns a cleanup function
return backButton.onClick(() => {
const isDeeplink = location.state?.fromDeeplink;
const isFirstPage = !window.history.state || window.history.state.idx === 0;
if (isDeeplink || isFirstPage) {
miniApp.close(); // Close the Mini App
} else {
navigate(-1); // Go back in history
}
});
}
backButton.hide();
}, [back, navigate, location]);
return <>{children}</>;
}
Signals are reactive values that update automatically. Use useSignal to subscribe to them in React:
import { useEffect } from 'react';
import { backButton, useSignal } from '@tma.js/sdk-react';
function BackButtonStatus() {
const isVisible = useSignal(backButton.isVisible);
useEffect(() => {
console.log('Back button is', isVisible ? 'visible' : 'hidden');
}, [isVisible]);
return null;
}
Init data contains user information and can be used for authentication:
import { initData } from '@tma.js/sdk-react';
function getUserId(): number | undefined {
try {
const user = initData.user();
return user?.id;
} catch (e) {
return undefined;
}
}
// Get start parameter (for deep linking)
const startParam = initData.startParam();
Launch parameters contain platform info, theme, and app data:
import { retrieveLaunchParams, useLaunchParams } from '@tma.js/sdk-react';
// In component
function Component() {
const launchParams = useLaunchParams();
// launchParams.tgWebAppPlatform - 'ios', 'android', 'macos', 'tdesktop', 'web', 'weba'
// launchParams.tgWebAppVersion - SDK version supported by client
// launchParams.tgWebAppData - init data
// launchParams.tgWebAppThemeParams - theme colors
// launchParams.tgWebAppStartParam - custom start parameter
}
// Outside component
const launchParams = retrieveLaunchParams();
Theme parameters are automatically provided by Telegram. Bind them to CSS variables:
import { themeParams, miniApp } from '@tma.js/sdk-react';
// During initialization
if (miniApp.mount.isAvailable()) {
themeParams.mount();
miniApp.mount();
themeParams.bindCssVars(); // Creates CSS variables like --tg-theme-bg-color
}
Available CSS variables after binding:
--tg-theme-bg-color--tg-theme-text-color--tg-theme-hint-color--tg-theme-link-color--tg-theme-button-color--tg-theme-button-text-color--tg-theme-secondary-bg-color--tg-theme-header-bg-color--tg-theme-accent-text-color--tg-theme-section-bg-color--tg-theme-section-header-text-color--tg-theme-subtitle-text-color--tg-theme-destructive-text-colorHandle viewport and safe areas for proper layout:
import { viewport } from '@tma.js/sdk-react';
if (viewport.mount.isAvailable()) {
viewport.mount().then(() => {
viewport.bindCssVars(); // Binds viewport dimensions to CSS
viewport.requestFullscreen(); // Request fullscreen mode
});
}
Available CSS variables:
/* Safe area insets */
padding-top: var(--tg-viewport-safe-area-inset-top, 0);
padding-bottom: var(--tg-viewport-safe-area-inset-bottom, 0);
/* Content safe area (for notch, etc.) */
padding-top: var(--tg-viewport-content-safe-area-inset-top, 0);
/* Viewport dimensions */
height: var(--tg-viewport-height);
width: var(--tg-viewport-width);
Usage in CSS:
.header {
padding-top: max(2rem, calc(var(--tg-viewport-content-safe-area-inset-top, 0) + var(--tg-viewport-safe-area-inset-top, 0)));
}
.footer {
padding-bottom: calc(1rem + var(--tg-viewport-safe-area-inset-bottom, 0));
}
For local development outside Telegram, mock the environment. See references/mock-env.md.
import { emitEvent, isTMA, mockTelegramEnv } from '@tma.js/sdk-react';
if (import.meta.env.DEV) {
if (!await isTMA('complete')) {
const themeParams = {
accent_text_color: '#6ab2f2',
bg_color: '#17212b',
button_color: '#5288c1',
button_text_color: '#ffffff',
destructive_text_color: '#ec3942',
header_bg_color: '#17212b',
hint_color: '#708499',
link_color: '#6ab3f3',
secondary_bg_color: '#232e3c',
section_bg_color: '#17212b',
section_header_text_color: '#6ab3f3',
subtitle_text_color: '#708499',
text_color: '#f5f5f5',
};
mockTelegramEnv({
onEvent(e) {
if (e.name === 'web_app_request_theme') {
return emitEvent('theme_changed', { theme_params: themeParams });
}
if (e.name === 'web_app_request_viewport') {
return emitEvent('viewport_changed', {
height: window.innerHeight,
width: window.innerWidth,
is_expanded: true,
is_state_stable: true,
});
}
if (e.name === 'web_app_request_safe_area') {
return emitEvent('safe_area_changed', { left: 0, top: 0, right: 0, bottom: 0 });
}
},
launchParams: new URLSearchParams([
['tgWebAppThemeParams', JSON.stringify(themeParams)],
['tgWebAppData', new URLSearchParams([
['auth_date', (Date.now() / 1000 | 0).toString()],
['hash', 'mock-hash'],
['signature', 'mock-signature'],
['user', JSON.stringify({ id: 1, first_name: 'Developer' })],
]).toString()],
['tgWebAppVersion', '8.4'],
['tgWebAppPlatform', 'tdesktop'],
]),
});
console.info('⚠️ Running in mocked Telegram environment');
}
}
Handle start parameters for deep linking. See references/deeplink.md.
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { initData } from "@tma.js/sdk-react";
export function useDeeplink() {
const navigate = useNavigate();
const processedRef = useRef(false);
useEffect(() => {
if (processedRef.current) return;
const startParam = initData.startParam();
if (!startParam) return;
processedRef.current = true;
try {
// startParam is base64url encoded
const base64 = startParam.replace(/-/g, '+').replace(/_/g, '/');
const decoded = atob(base64);
const params = new URLSearchParams(decoded);
const route = params.get('route');
if (route) {
navigate(route, { replace: true, state: { fromDeeplink: true } });
}
} catch (e) {
console.error("Failed to parse startParam:", e);
}
}, [navigate]);
}
Before using any method, check if it's available:
import { backButton } from '@tma.js/sdk-react';
// Option 1: Check before calling
if (backButton.show.isAvailable()) {
backButton.show();
}
// Option 2: Call only if available (safer, no-op if unavailable)
backButton.show.ifAvailable();
// Option 3: Mount only if available
backButton.mount.ifAvailable();
Components must be mounted before their methods can be used:
// ❌ Wrong - will throw error
backButton.show();
// ✅ Correct
backButton.mount();
backButton.show();
Telegram for macOS has known issues:
if (platform === 'macos') {
mockTelegramEnv({
onEvent(event, next) {
if (event.name === 'web_app_request_theme') {
const tp = themeParams.state() || retrieveLaunchParams().tgWebAppThemeParams;
return emitEvent('theme_changed', { theme_params: tp });
}
if (event.name === 'web_app_request_safe_area') {
return emitEvent('safe_area_changed', { left: 0, top: 0, right: 0, bottom: 0 });
}
next();
},
});
}
Never install both @tma.js/sdk and @tma.js/sdk-react:
// ❌ Wrong - causes bugs
{
"dependencies": {
"@tma.js/sdk": "^3.0.0",
"@tma.js/sdk-react": "^3.0.8"
}
}
// ✅ Correct - only the React package
{
"dependencies": {
"@tma.js/sdk-react": "^3.0.8"
}
}
Prevent accidental navigation:
if (swipeBehavior.isSupported()) {
swipeBehavior.mount();
swipeBehavior.disableVertical(); // Prevents swipe-to-close
}
For authentication, send init data to your server:
import { retrieveRawInitData } from '@tma.js/sdk-react';
const initDataRaw = retrieveRawInitData();
fetch('https://api.example.com/auth', {
method: 'POST',
headers: {
Authorization: `tma ${initDataRaw}`,
},
});
Server-side validation:
@tma.js/init-data-node for Node.jsMini Apps work on:
Make sure to call init() before using any SDK features.
Mount the component before calling its methods:
backButton.mount();
backButton.show();
Check availability before calling:
if (backButton.show.isAvailable()) {
backButton.show();
}
Use environment mocking during development and provide a fallback UI.
development
Generates a Typst laboratory report file report.typ from completed working code using BMSTU-style structure (title-bmstu, sections, images, codeblock, conclusions). Use when the user asks to create/fill a report.typ after implementation and testing of a lab task.
development
Create and format lab reports in Markdown with Typst-style headers. Validates line length ≤100 chars. Use when writing lab reports, formatting academic documents, or converting assignments to markdown reports.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.