telegram-mini-app-skill について
Web 開発者向けの、フレームワークに依存しない Telegram Mini Apps 開発ガイドです。WebApp SDK、UI コンポーネント、テーマ、データ保護、テストを扱います。
主な機能
- 01WebApp SDK の導入とライフサイクル
- 02テーマ変数とテーマ変更
- 03ナビゲーションとボタン API
- 04触覚フィードバック、ポップアップ、アラート
- 05ストレージとデバイス API
- 06セキュリティ、テスト、デバッグ
活用シーン
- 01選んだ Web フレームワークで Mini App を始める
- 02Telegram のテーマと UI にアプリを適応する
- 03ネイティブのナビゲーションとボタン動作を追加する
- 04データ検証やテスト方法を確認する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rithprohos/ telegram-mini-app-skills --skill telegram-mini-app-skillインストール後、Claude Code で/telegram-mini-app-skillを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
Telegram ミニアプリ開発ガイド
公式リファレンス:core.telegram.org/bots/webapps
このスキルは、Telegram ミニアプリを構築するためのフレームワークに依存しないガイドです。React、Vue、Svelte、Angular、Solid、またはバニラ HTML/JS のいずれを使用していても、このガイドでは共通の SDK の概念、テーマシステム、ベストプラクティスを取り上げます。
目次
- Telegram ミニアプリとは?
- SDK の読み込み
- WebApp オブジェクト
- 初期化のライフサイクル
- テーマ設定と CSS 変数
- ナビゲーションコンポーネント
- 下部ボタン(MainButton と SecondaryButton)
- 触覚フィードバック
- ポップアップとアラート
- データの検証とセキュリティ
- ストレージ API
- 全画面モード
- セーフエリア
- 利用可能なイベント
- フレームワーク別の統合例
- デザインガイドライン
- テストとデバッグ
- よくある落とし穴
1. Telegram ミニアプリとは?
Telegram ミニアプリは、Telegram メッセンジャー内で動作するウェブアプリケーション(HTML、CSS、JavaScript)です。次の方法で起動できます。
- ボットメニュー — BotFather のメニューボタン設定を使用
- インラインボタン — ボットメッセージ内の
web_appタイプ - 添付ファイルメニュー — ボットを添付ファイルメニューに追加
- 直接リンク —
t.me/botname/appname - キーボードボタン —
web_appタイプのキーボードボタン - ホーム画面のショートカット — ユーザーはミニアプリをホーム画面に追加できます
主な特徴:
- HTTPS 経由で配信する必要があります
- すべてのプラットフォーム(iOS、Android、デスクトップ、ウェブ)で WebView 内で動作します
- Telegram のユーザーデータ、テーマ、ネイティブ UI コンポーネントにアクセスできます
- @BotFather で作成したボットに関連付けられます
2. SDK の読み込み
オプション A:スクリプトタグ(すべてのフレームワーク)
<head> 内で、ほかのスクリプトより前に配置します。
<script src="https://telegram.org/js/telegram-web-app.js"></script>
これにより window.Telegram.WebApp オブジェクトが作成され、テーマ設定用の CSS 変数が自動的に挿入されます。
オプション B:npm パッケージ
バンドラーを使用する最新の JavaScript プロジェクト向けです。
# Official community SDK (TypeScript-first, tree-shakeable)
npm install @telegram-apps/sdk
# Alternative lightweight wrapper
npm install @twa-dev/sdk
フレームワーク別の読み込み方法
| フレームワーク | 読み込み方法 |
|---|---|
| バニラ HTML | <head> 内の <script> タグ |
| React / Next.js | next/script の <Script src="..." strategy="beforeInteractive" />、または index.html 内の <script> |
| Vue / Nuxt | index.html 内の <script>、または useHead() を使った Nuxt プラグイン |
| Svelte / SvelteKit | <svelte:head> タグ、または app.html |
| Angular | angular.json の scripts 配列、または index.html に追加 |
3. WebApp オブジェクト
SDK を読み込むと、メイン API を次の場所から利用できます。
const tg = window.Telegram.WebApp;
主なプロパティ
| プロパティ | 型 | 説明 |
|---|---|---|
initData | string | バックエンドでの検証に使用する生の初期化データ文字列 |
initDataUnsafe | object | 解析済みの初期化データ(ユーザー、チャットなど)— 検証されていないため、クライアント側では信頼しないでください |
version | string | SDK のバージョン(例:"8.0") |
platform | string | プラットフォーム識別子("android"、"ios"、"tdesktop"、"web"、"unknown") |
colorScheme | string | "light" または "dark" |
themeParams | object | キーと値のペアで表される現在のテーマカラー |
isExpanded | boolean | ミニアプリが全画面の高さまで展開されているかどうか |
viewportHeight | number | 現在のビューポートの高さ(ピクセル単位) |
viewportStableHeight | number | 安定したビューポートの高さ(キーボードの表示による変化なし) |
headerColor | string | 現在のヘッダー色(#RRGGBB) |
backgroundColor | string | 現在の背景色(#RRGGBB) |
bottomBarColor | string | 現在のボトムバーの色(#RRGGBB) |
isFullscreen | boolean | 全画面モードが有効かどうか |
主なサブオブジェクト
| サブオブジェクト | 説明 |
|---|---|
BackButton | ヘッダーの戻るボタンを制御します |
MainButton | メインの下部ボタンを制御します |
SecondaryButton | 2つ目の下部ボタンを制御します |
SettingsButton | コンテキストメニューの設定項目を制御します |
HapticFeedback | 触覚フィードバック/振動フィードバックを制御します |
CloudStorage | クラウドのキーと値によるストレージ(最大1024項目、デバイス間で同期) |
DeviceStorage | 永続的なローカルストレージ(最大5 MB、デバイス内のみ) |
SecureStorage | OS のキーチェーン/キーストアを使用した暗号化ストレージ(最大10項目) |
BiometricManager | 生体認証 |
Accelerometer | 加速度センサーのデータ |
Gyroscope | ジャイロスコープのデータ |
DeviceOrientation | デバイスの向きのデータ |
LocationManager | 位置情報へのアクセス |
4. 初期化のライフサイクル
すべてのミニアプリは、アプリを表示する準備ができたことを Telegram に知らせるため、tg.ready() を呼び出す必要があります。
あらゆるフレームワークで使える共通パターン
// 1. Get the WebApp instance
const tg = window.Telegram.WebApp;
// 2. Signal ready
tg.ready();
// 3. Expand to full height (recommended)
tg.expand();
// 4. Access user data
const user = tg.initDataUnsafe?.user;
console.log(user?.first_name, user?.username);
ready() が重要な理由
ready()が呼び出されるまで、Telegram は読み込み中のプレースホルダーを表示します- アプリの初期 UI がレンダリングされたら、できるだけ早く呼び出してください
- 表示できる内容がアプリにない段階では呼び出さないでください
5. テーマ設定と CSS 変数
基本ルール
色をハードコードしてはいけません。Telegram の SDK が自動的に挿入する CSS 変数を使用してください。これにより、アプリはライトモード、ダークモード、およびすべてのプラットフォームでネイティブな外観になります。
利用可能な CSS 変数
SDK は、以下の CSS 変数を <html> 要素に自動的に設定します。
| CSS 変数 | JS プロパティ(themeParams.X) | 説明 |
|---|---|---|
--tg-theme-bg-color | bg_color | メインの背景 |
--tg-theme-text-color | text_color | メインのテキスト |
--tg-theme-hint-color | hint_color | セカンダリ/ヒントテキスト |
--tg-theme-link-color | link_color | リンク |
--tg-theme-button-color | button_color | メインボタンの背景 |
--tg-theme-button-text-color | button_text_color | ボタンのテキスト |
--tg-theme-secondary-bg-color | secondary_bg_color | セカンダリの背景(カード、セクション) |
--tg-theme-header-bg-color | header_bg_color | ヘッダーの背景 |
--tg-theme-bottom-bar-bg-color | bottom_bar_bg_color | ボトムバーの背景 |
--tg-theme-accent-text-color | accent_text_color | アクセントテキスト |
--tg-theme-section-bg-color | section_bg_color | セクション/カードの背景 |
--tg-theme-section-header-text-color | section_header_text_color | セクションヘッダーのテキスト |
--tg-theme-section-separator-color | section_separator_color | 区切り線 |
--tg-theme-subtitle-text-color | subtitle_text_color | サブタイトルのテキスト |
--tg-theme-destructive-text-color | destructive_text_color | 破壊的操作/危険を示すテキスト |
追加の CSS 変数
| CSS 変数 | 説明 |
|---|---|
--tg-color-scheme | "light" または "dark" |
--tg-viewport-height | 現在のビューポートの高さ |
--tg-viewport-stable-height | 安定したビューポートの高さ |
--tg-safe-area-inset-top | システムのセーフエリア(上) |
--tg-safe-area-inset-bottom | システムのセーフエリア(下) |
--tg-safe-area-inset-left | システムのセーフエリア(左) |
--tg-safe-area-inset-right | システムのセーフエリア(右) |
--tg-content-safe-area-inset-top | Telegram UI のセーフエリア(上) |
--tg-content-safe-area-inset-bottom | Telegram UI のセーフエリア(下) |
--tg-content-safe-area-inset-left | Telegram UI のセーフエリア(左) |
--tg-content-safe-area-inset-right | Telegram UI のセーフエリア(右) |
使用例
✅ 正しい例 — CSS 変数を使用する
/* CSS */
body {
background-color: var(--tg-theme-bg-color, #ffffff);
color: var(--tg-theme-text-color, #000000);
}
.card {
background-color: var(--tg-theme-section-bg-color, #f5f5f5);
border: 1px solid var(--tg-theme-section-separator-color, #e0e0e0);
}
.button-primary {
background-color: var(--tg-theme-button-color, #3b82f6);
color: var(--tg-theme-button-text-color, #ffffff);
}
.hint-text {
color: var(--tg-theme-hint-color, #999999);
}
必ずフォールバック値を指定してください(
var()の2番目の引数)。開発中に Telegram の外でもアプリが動作するようになります。
❌ 誤った例 — 色をハードコードする
/* NEVER do this */
body {
background-color: #ffffff;
color: #000000;
}
.dark body {
background-color: #1a1a1a;
color: #ffffff;
}
短縮変数システムの作成(推奨)
使いやすいように、Telegram の冗長な変数をグローバル CSS で短い名前に割り当てます。
:root {
--bg: var(--tg-theme-bg-color, #ffffff);
--fg: var(--tg-theme-text-color, #171717);
--btn: var(--tg-theme-button-color, #3b82f6);
--btn-text: var(--tg-theme-button-text-color, #ffffff);
--hint: var(--tg-theme-hint-color, #a1a1aa);
--link: var(--tg-theme-link-color, #007aff);
--secondary-bg: var(--tg-theme-secondary-bg-color, #efeff4);
--section-bg: var(--tg-theme-section-bg-color, #ffffff);
--section-header: var(--tg-theme-section-header-text-color, #6d6d72);
--subtitle: var(--tg-theme-subtitle-text-color, #8e8e93);
--destructive: var(--tg-theme-destructive-text-color, #ff3b30);
--accent: var(--tg-theme-accent-text-color, #007aff);
--separator: var(--tg-theme-section-separator-color, #c8c7cc);
}
テーマ変更の監視
tg.onEvent("themeChanged", () => {
// Theme params have been updated
// CSS variables are auto-updated by the SDK
// If using JS theming, re-read tg.themeParams
console.log("New color scheme:", tg.colorScheme);
});
6. ナビゲーションコンポーネント
BackButton
Telegram ヘッダーのネイティブの戻るボタンを制御します。独自の戻るボタンを作成する代わりに、これを使用してください。
// Show the back button
tg.BackButton.show();
// Handle clicks
tg.BackButton.onClick(() => {
// Navigate back in your app's routing
history.back(); // or your router's back method
});
// Hide when no longer needed
tg.BackButton.hide();
// Remove a specific handler
tg.BackButton.offClick(handler);
SettingsButton
Mini App のコンテキストメニュー(三点メニュー)に「設定」オプションを追加します。
tg.SettingsButton.show();
tg.SettingsButton.onClick(() => {
// Navigate to settings page
});
7. ボトムボタン
Telegram には、ボトムボタンが2つあります。MainButton(メイン)と SecondaryButton(セカンダリ)です。Mini App の下部に固定表示されます。
MainButton
const btn = tg.MainButton;
// Configure and show
btn.setText("SUBMIT ORDER");
btn.show();
// Or use setParams for multiple properties at once
btn.setParams({
text: "SUBMIT ORDER",
color: tg.themeParams.button_color,
text_color: tg.themeParams.button_text_color,
is_active: true,
is_visible: true,
has_shine_effect: false,
});
// Handle click
btn.onClick(() => {
// Perform the action
});
// Show loading state
btn.showProgress(true); // true = leave button active
btn.disable(); // prevent double-clicks
// Reset after completion
btn.hideProgress();
btn.enable();
btn.hide();
SecondaryButton
MainButton と同じように動作します。
tg.SecondaryButton.setParams({
text: "CANCEL",
is_visible: true,
});
tg.SecondaryButton.onClick(() => {
// Handle secondary action
});
BottomButton のプロパティ
| プロパティ/メソッド | 説明 |
|---|---|
.text | ボタンのテキスト |
.color | 背景色 |
.textColor | テキストの色 |
.isVisible | ボタンが表示されているかどうか |
.isActive | ボタンがクリック可能かどうか |
.isProgressVisible | 読み込みスピナーが表示されているかどうか |
.setText(text) | ボタンのテキストを更新 |
.show() / .hide() | 表示/非表示を切り替え |
.enable() / .disable() | クリック可能かどうかを切り替え |
.showProgress(leaveActive) | スピナーを表示 |
.hideProgress() | スピナーを非表示 |
.setParams(params) | 複数のプロパティを一度に設定 |
.onClick(cb) | クリックハンドラーを登録 |
.offClick(cb) | クリックハンドラーを削除 |
重要:ハンドラーのクリーンアップ
クリックハンドラーが不要になったら、必ず削除してください(コンポーネントのアンマウント時やページ遷移時など)。削除しないと、ゴーストハンドラーが発生することがあります。
8. ハプティックフィードバック
触覚フィードバックとして、ネイティブの振動を発生させます。
const haptic = tg.HapticFeedback;
// Impact feedback — for button presses, interactions
haptic.impactOccurred("light"); // light tap
haptic.impactOccurred("medium"); // medium tap
haptic.impactOccurred("heavy"); // strong tap
haptic.impactOccurred("rigid"); // rigid tap
haptic.impactOccurred("soft"); // soft tap
// Notification feedback — for results/outcomes
haptic.notificationOccurred("success"); // operation succeeded
haptic.notificationOccurred("error"); // operation failed
haptic.notificationOccurred("warning"); // caution
// Selection feedback — for selection changes
haptic.selectionChanged(); // e.g., picker/slider changes
使用する場面
| シナリオ | 種類 |
|---|---|
| ボタンのタップ | impactOccurred("light") |
| フォーム送信の成功 | notificationOccurred("success") |
| エラー応答 | notificationOccurred("error") |
| スイッチの切り替え | impactOccurred("medium") |
| スライダー/ピッカーの値変更 | selectionChanged() |
| 項目の削除 | impactOccurred("heavy") |
| クリア/リセット操作 | impactOccurred("light") |
9. ポップアップとアラート
ブラウザーの alert()/confirm() の代わりに、Telegram のネイティブダイアログを使用します。
// Simple alert
tg.showAlert("File saved successfully!", () => {
// Callback after user dismisses
});
// Confirmation dialog
tg.showConfirm("Are you sure you want to delete this?", (confirmed) => {
if (confirmed) {
// User tapped "OK"
}
});
// Custom popup with buttons
tg.showPopup(
{
title: "Choose an option", // optional
message: "What would you like to do?",
buttons: [
{ id: "save", type: "default", text: "Save" },
{ id: "delete", type: "destructive", text: "Delete" },
{ id: "cancel", type: "cancel" }, // text is auto-set
],
},
(buttonId) => {
switch (buttonId) {
case "save":
/* ... */ break;
case "delete":
/* ... */ break;
case "cancel":
/* ... */ break;
}
},
);
ポップアップのボタンの種類
| 種類 | 動作 |
|---|---|
"default" | 通常のボタン |
"ok" | 「OK」のテキストを表示し、ポップアップを閉じます |
"close" | 「Close」のテキストを表示し、ポップアップを閉じます |
"cancel" | 「Cancel」のテキストを表示し、ポップアップを閉じます |
"destructive" | 赤色/破壊的なスタイルのボタン |
10. データの検証とセキュリティ
クライアント側のデータ
tg.initDataUnsafe から解析済みのユーザーデータを取得できますが、偽装される可能性があります。必ずバックエンドで検証してください。
// Client: send initData to your backend
const response = await fetch("/api/your-endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Telegram-Init-Data": tg.initData, // raw string for validation
},
body: JSON.stringify({
/* your payload */
}),
});
サーバー側の検証(HMAC-SHA256)
バックエンドでは、ボットのトークンを使って initData を検証する必要があります。
1. Parse initData as query string
2. Extract `hash` parameter, keep remaining fields
3. Sort remaining fields alphabetically
4. Create data_check_string = sorted fields joined with '\n' in "key=value" format
5. secret_key = HMAC_SHA256(bot_token, "WebAppData")
6. Compare: HMAC_SHA256(data_check_string, secret_key) == hash
7. Check auth_date for freshness
疑似コード:
data_check_string = "auth_date=<auth_date>\nquery_id=<query_id>\nuser=<user>"
secret_key = HMAC_SHA256(<bot_token>, "WebAppData")
if hex(HMAC_SHA256(data_check_string, secret_key)) == hash:
# Data is genuine and from Telegram
サードパーティでの検証(Ed25519 署名)
ボットのトークンを持たないサードパーティとデータを共有する場合は、Telegram の公開鍵を使って signature フィールドを検証します。
- 本番環境:
e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d - テスト環境:
40055058a4ee38156a06562e52eece92a771bcd8346a8c4615cb7376eddf72ec
11. ストレージ API
CloudStorage(デバイス間で同期)
最大1024個のキーと値のペアを保存できます。値あたり4096バイトで、Telegram のサーバー経由で同期されます。
tg.CloudStorage.setItem("theme_pref", "dark", (err, success) => {});
tg.CloudStorage.getItem("theme_pref", (err, value) => {});
tg.CloudStorage.getItems(["key1", "key2"], (err, values) => {});
tg.CloudStorage.removeItem("theme_pref", (err, success) => {});
tg.CloudStorage.getKeys((err, keys) => {});
DeviceStorage(ローカルのみ、Bot API 9.0+)
最大5 MBを保存でき、デバイス上に保持されます。localStorage に似ています。
tg.DeviceStorage.setItem("draft", longText);
tg.DeviceStorage.getItem("draft");
SecureStorage(暗号化、Bot API 9.0+)
最大10個の項目を保存でき、OS の Keychain(iOS)/Keystore(Android)を使用します。
tg.SecureStorage.setItem("auth_token", token);
tg.SecureStorage.getItem("auth_token");
12. 全画面モード
Bot API 8.0(2024 年 11 月)から利用できます。Mini App が画面全体を使用できるようになります。
// Enter full-screen
tg.requestFullscreen();
// Exit full-screen
tg.exitFullscreen();
// Check status
console.log(tg.isFullscreen);
// Listen for changes
tg.onEvent("fullscreenChanged", () => {
console.log("Fullscreen:", tg.isFullscreen);
});
tg.onEvent("fullscreenFailed", (event) => {
console.log("Fullscreen failed:", event.error);
});
重要:全画面モードでは、システム UI 要素(ノッチ、ステータスバーなど)とコンテンツが重ならないように、必ずセーフエリアを処理してください。
13. セーフエリア
特に全画面モードでは、次の 2 種類のセーフエリアを処理する必要があります。
システムセーフエリア(ノッチ、ステータスバー、ホームインジケーター)
.content {
padding-top: var(--tg-safe-area-inset-top, 0px);
padding-bottom: var(--tg-safe-area-inset-bottom, 0px);
padding-left: var(--tg-safe-area-inset-left, 0px);
padding-right: var(--tg-safe-area-inset-right, 0px);
}
コンテンツセーフエリア(Telegram 独自のヘッダー、下部バー)
React/Next.js などのフレームワークでは、これらをドキュメントの body に直接適用しないことを強くおすすめします。Telegram の Webview によるインジェクションが原因で、100vw やスケーリングの制約によってコンテンツが水平方向に画面外へはみ出したり、縮小されたりすることがあるためです。
代わりに、あらゆるバージョンの Webview で最も確実に機能する方法は、JavaScript と CSS を組み合わせたフォールバックです。contentSafeAreaInset.top(Telegram UI)と safeAreaInset.top(モバイルデバイスのノッチ)を組み合わせることで、重なりを完全に防げます。
1. Telegram の初期化ループ内で、パディングを計算してネイティブに結合します。
// Add base safeArea (device notches) + contentSafe (Telegram header ui) together
// Apply a hard fallback (e.g., 48px) just in case Telegram fails to inject variables entirely
const top = Math.max(
48,
(tg.contentSafeAreaInset?.top || 0) + (tg.safeAreaInset?.top || 0),
);
const bottom = Math.max(
32,
(tg.contentSafeAreaInset?.bottom || 0) + (tg.safeAreaInset?.bottom || 0),
);
// Explicitly inject safe bounds into your root HTML variables
document.documentElement.style.setProperty("--safe-top", `${top}px`);
document.documentElement.style.setProperty("--safe-bottom", `${bottom}px`);
2. CSS(globals.css や Tailwind の設定など)で、挿入された変数を参照します。
:root {
/* You can optionally add env() fallbacks here if you want native browser backup */
--safe-top: env(safe-area-inset-top, 48px);
--safe-bottom: env(safe-area-inset-bottom, 32px);
}
3. 最後に、React の個々のページラッパーでパディングを指定します。
<main
style={{
// Add your desired extra padding (e.g., 1rem) to the base safe area boundary mathematically
paddingTop: "calc(1rem + var(--safe-top))",
paddingBottom: "calc(2rem + var(--safe-bottom))",
}}
>
{/* Content goes here safely below the Close/Settings notch */}
</main>
セーフエリアの変更を監視する
tg.onEvent("safeAreaChanged", () => {
// System safe area changed (e.g., orientation change)
});
tg.onEvent("contentSafeAreaChanged", () => {
// Telegram UI safe area changed
});
14. 利用可能なイベント
tg.onEvent(eventType, handler) でハンドラーを登録し、tg.offEvent(eventType, handler) で削除します。
| イベント | 発生条件 |
|---|---|
activated | Mini App がアクティブ/表示状態になる |
deactivated | Mini App が非アクティブ/非表示状態になる |
themeChanged | ユーザーが Telegram のテーマを変更した |
viewportChanged | ビューポートの高さが変わった(キーボード、展開) |
safeAreaChanged | システムセーフエリアのインセットが変わった |
contentSafeAreaChanged | Telegram のコンテンツセーフエリアが変わった |
mainButtonClicked | メインボタンが押された |
secondaryButtonClicked | セカンダリボタンが押された |
backButtonClicked | 戻るボタンが押された |
settingsButtonClicked | 設定ボタンが押された |
invoiceClosed | 支払い請求書が閉じられた |
popupClosed | ポップアップが閉じられた |
qrTextReceived | QR コードがスキャンされた |
clipboardTextReceived | クリップボードのテキストを受信した |
writeAccessRequested | 書き込みアクセス許可の結果 |
contactRequested | 連絡先共有の結果 |
fullscreenChanged | 全画面状態が変わった |
fullscreenFailed | 全画面への切り替え要求に失敗した |
homeScreenAdded | アプリがホーム画面に追加された |
homeScreenChecked | ホーム画面の状態が確認された |
biometricManagerUpdated | 生体認証マネージャーの状態が変わった |
biometricAuthRequested | 生体認証の結果 |
shareMessageSent | 共有メッセージが送信された |
shareMessageFailed | 共有メッセージの送信に失敗した |
emojiStatusSet | 絵文字ステータスが設定された |
emojiStatusFailed | 絵文字ステータスの設定に失敗した |
fileDownloadRequested | ファイルダウンロード要求の結果 |
15. フレームワーク連携レシピ
Vanilla JavaScript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--tg-theme-bg-color, #fff);
color: var(--tg-theme-text-color, #000);
}
.card {
background: var(--tg-theme-section-bg-color, #f5f5f5);
border-radius: 14px;
padding: 16px;
margin: 16px;
}
.hint {
color: var(--tg-theme-hint-color, #999);
font-size: 13px;
}
</style>
</head>
<body>
<div class="card">
<h1>Hello, <span id="username">User</span>!</h1>
<p class="hint">Welcome to the Mini App</p>
</div>
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
const user = tg.initDataUnsafe?.user;
if (user) {
document.getElementById("username").textContent = user.first_name;
}
tg.MainButton.setText("DONE");
tg.MainButton.show();
tg.MainButton.onClick(() => {
tg.HapticFeedback.notificationOccurred("success");
tg.close();
});
</script>
</body>
</html>
React / Next.js
// hooks/useTelegram.ts — Reusable hook
import { useEffect, useState, useCallback } from "react";
export function useTelegram() {
const [tg, setTg] = useState<TelegramWebApp | null>(null);
const [user, setUser] = useState<TelegramUser | null>(null);
useEffect(() => {
if (typeof window !== "undefined" && window.Telegram?.WebApp) {
const webapp = window.Telegram.WebApp;
webapp.ready();
webapp.expand();
setTg(webapp);
setUser(webapp.initDataUnsafe?.user || null);
}
}, []);
const showMainButton = useCallback(
(text: string, onClick: () => void) => {
if (!tg) return;
tg.MainButton.setText(text);
tg.MainButton.show();
tg.MainButton.onClick(onClick);
return () => tg.MainButton.offClick(onClick);
},
[tg],
);
const showBackButton = useCallback(
(onClick: () => void) => {
if (!tg) return;
tg.BackButton.show();
tg.BackButton.onClick(onClick);
return () => {
tg.BackButton.offClick(onClick);
tg.BackButton.hide();
};
},
[tg],
);
return { tg, user, showMainButton, showBackButton };
}
// pages/MyPage.tsx
"use client";
import { useEffect } from "react";
import { useTelegram } from "@/hooks/useTelegram";
export default function MyPage() {
const { tg, user, showBackButton } = useTelegram();
useEffect(() => {
return showBackButton?.(() => window.history.back());
}, [showBackButton]);
return (
<main style={{ background: "var(--tg-theme-secondary-bg-color)" }}>
<h1 style={{ color: "var(--tg-theme-text-color)" }}>
Hello, {user?.first_name}!
</h1>
</main>
);
}
Next.js のレイアウト設定
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Script
src="https://telegram.org/js/telegram-web-app.js"
strategy="beforeInteractive"
/>
{children}
</body>
</html>
);
}
suppressHydrationWarningは必須です。Telegram のスクリプトが SSR/ハイドレーション中に DOM へスタイル属性を挿入するためです。
Vue 3 / Nuxt
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const tg = ref<any>(null);
const user = ref<any>(null);
export function useTelegram() {
onMounted(() => {
if (window.Telegram?.WebApp) {
tg.value = window.Telegram.WebApp;
tg.value.ready();
tg.value.expand();
user.value = tg.value.initDataUnsafe?.user || null;
}
});
function showBackButton(onClick: () => void) {
tg.value?.BackButton.show();
tg.value?.BackButton.onClick(onClick);
onUnmounted(() => {
tg.value?.BackButton.offClick(onClick);
tg.value?.BackButton.hide();
});
}
return { tg, user, showBackButton };
}
</script>
<template>
<main :style="{ background: 'var(--tg-theme-secondary-bg-color)' }">
<h1 :style="{ color: 'var(--tg-theme-text-color)' }">
Hello, {{ user?.first_name }}!
</h1>
</main>
</template>
<script setup lang="ts">
import { useTelegram } from "@/composables/useTelegram";
import { useRouter } from "vue-router";
const { user, showBackButton } = useTelegram();
const router = useRouter();
showBackButton(() => router.back());
</script>
Svelte / SvelteKit
<script context="module" lang="ts">
export function getTelegram() {
if (typeof window !== 'undefined' && window.Telegram?.WebApp) {
return window.Telegram.WebApp;
}
return null;
}
</script>
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { getTelegram } from '$lib/telegram';
let user: any = null;
onMount(() => {
const tg = getTelegram();
if (tg) {
tg.ready();
tg.expand();
user = tg.initDataUnsafe?.user;
tg.BackButton.show();
tg.BackButton.onClick(() => history.back());
}
});
onDestroy(() => {
const tg = getTelegram();
tg?.BackButton.hide();
});
</script>
<main style="background: var(--tg-theme-secondary-bg-color); color: var(--tg-theme-text-color);">
<h1>Hello, {user?.first_name ?? 'User'}!</h1>
</main>
SvelteKit のレイアウト
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
%sveltekit.head%
</head>
<body>
%sveltekit.body%
</body>
</html>
16. デザインガイドライン
Mini Apps 向けの Telegram 公式デザイン原則です。
基本原則
- モバイルファースト:すべての要素はレスポンシブで、モバイルのビューポートに最適化する必要があります
- ネイティブな操作感:既存の Telegram UI コンポーネントのスタイルと動作を再現します
- 滑らかなアニメーション:すべてのアニメーションで 60fps を目指します
- アクセシビリティ:すべての入力欄と画像にラベルを設定します
- テーマへの適応:すべての色に Telegram の CSS 変数を使用します
- セーフエリアへの配慮:特に全画面モードでは、セーフエリアを考慮します
- パフォーマンスへの配慮:Android では、User-Agent でデバイスのパフォーマンスクラスを確認し、ローエンドデバイスではアニメーションを減らします
推奨 UI パターン
| 要素 | 推奨スタイル |
|---|---|
| ページの背景 | var(--tg-theme-secondary-bg-color) |
| カード/セクション | var(--tg-theme-section-bg-color) と border-radius: 14px |
| セクションラベル | 大文字、小さめのフォント、var(--tg-theme-section-header-text-color) |
| ヒントテキスト | 小さめのフォント、var(--tg-theme-hint-color) |
| 区切り線 | var(--tg-theme-section-separator-color) を使った 1px の線 |
| 入力欄のフォントサイズ | 16px 以上(iOS でフォーカス時に自動ズームされるのを防ぎます) |
| ボタン | 主なアクションには MainButton を使うか、var(--tg-theme-button-color) を使ったカスタムボタンを使用します |
| 破壊的なアクション | var(--tg-theme-destructive-text-color) |
| 下部の余白 | MainButton と重ならないよう、十分な余白(約 80px)を追加します |
17. テストとデバッグ
開発環境
- 開発サーバーを
localhostで起動します - HTTPS トンネル経由で公開します:
ngrok http 3000(または Cloudflare Tunnel、localtunnel など) - BotFather を設定します:
- ボットを作成/選択します
/setmenubuttonまたは/newappで Web App URL を設定します
- Telegram で開いてテストします
デバッグのヒント
| プラットフォーム | 方法 |
|---|---|
| Android | Android の開発者向け設定で WebView のデバッグを有効にする → chrome://inspect |
| iOS | Safari → Develop → お使いのデバイス → WebView |
| デスクトップ | Mini App 内を右クリック →「要素を検証」(一部のビルドで利用可能) |
| Telegram Web | 通常のブラウザー DevTools |
Telegram 外でのテスト
すべての tg 呼び出しを安全確認で囲みます。開発用のフォールバック UI を用意してください。
const tg = window.Telegram?.WebApp;
const isInTelegram = !!tg?.initData;
if (isInTelegram) {
tg.ready();
tg.MainButton.show();
} else {
// Show a regular HTML button instead for local testing
console.log("Running outside Telegram — showing fallback UI");
}
18. よくある落とし穴
| 落とし穴 | 解決策 |
|---|---|
| Telegram で色が正しく表示されない | --tg-theme-* CSS 変数を使い、値をハードコードしないでください |
| アプリが読み込み中のスピナーから進まない | 初回のレンダリング後に tg.ready() を呼び出してください |
| MainButton が反応しない | 新しいハンドラーを登録する前に、必ず offClick(handler) を呼び出してください |
| iOS で入力欄にフォーカスするとズームインする | 入力欄の font-size を 16px 以上に設定してください |
| SSR のハイドレーションエラー(Next.js、Nuxt) | <html> タグに suppressHydrationWarning を追加してください |
| initData が空である | アプリはキーボードボタンから起動されています(想定どおり)。適切に処理してください |
| API 呼び出しが 403 を返す | サーバーで initData を正しく検証し、auth_date が新しいことを確認してください |
| ボタンハンドラー内の状態が古い | refs(React)またはリアクティブなゲッター(Vue)を使ってください。ハンドラーはクロージャー内の変数をキャプチャします |
| MainButton の背後にコンテンツが隠れる | メインコンテンツに下部のパディング(約 80~100px)を追加してください |
| 全画面表示でセーフエリア内のコンテンツが重なる | --tg-safe-area-inset-* と --tg-content-safe-area-inset-* CSS 変数を使ってください |
window.Telegram が undefined である | SDK スクリプトがまだ読み込まれていません。アプリのコードより先に読み込まれるようにしてください |
| MainButton が二重クリックされる | クリック直後にボタンを無効化し、進行状況を表示してください |
| テーマが動的に更新されない | themeChanged イベントをリッスンしてください。CSS 変数は自動的に更新されます |
| クリップボード API が機能しない | ブラウザーの Clipboard API ではなく、tg.readTextFromClipboard() を使ってください |
| Web でファイルをダウンロードできない | レスポンスヘッダーに Content-Disposition と Access-Control-Allow-Origin: https://web.telegram.org を含めてください |
クイックリファレンス:便利なメソッド
const tg = window.Telegram.WebApp;
// Basics
tg.ready(); // Signal app is ready
tg.expand(); // Expand to full height
tg.close(); // Close the Mini App
// User data
tg.initData; // Raw string for server validation
tg.initDataUnsafe.user; // User object (not validated!)
// Navigation
tg.openLink(url); // Open URL in browser
tg.openLink(url, { try_instant_view: true }); // Try Instant View
tg.openTelegramLink(url); // Open telegram link (resolves inside Telegram)
// Sharing
tg.switchInlineQuery(query, ["users", "groups"]); // Switch to inline mode
tg.shareToStory(mediaUrl, params); // Share to Stories
// Clipboard
tg.readTextFromClipboard((text) => {}); // Read clipboard
// QR Code
tg.showScanQrPopup({ text: "Scan QR" }, (data) => {
// Handle QR data
return true; // return true to close popup
});
tg.closeScanQrPopup();
// Payments
tg.openInvoice(invoiceUrl, (status) => {
// status: "paid", "cancelled", "failed", "pending"
});
// Colors
tg.setHeaderColor("#RRGGBB"); // Custom header color
tg.setBackgroundColor("#RRGGBB"); // Custom background color
tg.setBottomBarColor("#RRGGBB"); // Custom bottom bar color
// Permissions
tg.requestWriteAccess((granted) => {}); // Request PM write access
tg.requestContact((shared) => {}); // Request phone contact
// Home screen
tg.addToHomeScreen(); // Prompt add to home screen
tg.checkHomeScreenStatus((status) => {}); // 'added', 'missed', etc.
// Fullscreen
tg.requestFullscreen();
tg.exitFullscreen();
// Closing confirmation
tg.enableClosingConfirmation(); // Warn user before closing
tg.disableClosingConfirmation();
// Send data to bot (limited to 4096 bytes)
tg.sendData(JSON.stringify({ action: "submit" }));