mcpField
T
telegram-mini-app-skill

telegram-mini-app-skill

github.com/rithprohos開発14122 回インストール36 週間前に更新
リポジトリ

telegram-mini-app-skill について

Web 開発者向けの、フレームワークに依存しない Telegram Mini Apps 開発ガイドです。WebApp SDK、UI コンポーネント、テーマ、データ保護、テストを扱います。

主な機能

  1. 01WebApp SDK の導入とライフサイクル
  2. 02テーマ変数とテーマ変更
  3. 03ナビゲーションとボタン API
  4. 04触覚フィードバック、ポップアップ、アラート
  5. 05ストレージとデバイス API
  6. 06セキュリティ、テスト、デバッグ

活用シーン

  1. 01選んだ Web フレームワークで Mini App を始める
  2. 02Telegram のテーマと UI にアプリを適応する
  3. 03ネイティブのナビゲーションとボタン動作を追加する
  4. 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 の概念、テーマシステム、ベストプラクティスを取り上げます。


目次

  1. Telegram ミニアプリとは?
  2. SDK の読み込み
  3. WebApp オブジェクト
  4. 初期化のライフサイクル
  5. テーマ設定と CSS 変数
  6. ナビゲーションコンポーネント
  7. 下部ボタン(MainButton と SecondaryButton)
  8. 触覚フィードバック
  9. ポップアップとアラート
  10. データの検証とセキュリティ
  11. ストレージ API
  12. 全画面モード
  13. セーフエリア
  14. 利用可能なイベント
  15. フレームワーク別の統合例
  16. デザインガイドライン
  17. テストとデバッグ
  18. よくある落とし穴

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.jsnext/script の <Script src="..." strategy="beforeInteractive" />、または index.html 内の <script>
Vue / Nuxtindex.html 内の <script>、または useHead() を使った Nuxt プラグイン
Svelte / SvelteKit<svelte:head> タグ、または app.html
Angularangular.json の scripts 配列、または index.html に追加

3. WebApp オブジェクト

SDK を読み込むと、メイン API を次の場所から利用できます。

const tg = window.Telegram.WebApp;
主なプロパティ
プロパティ型説明
initDatastringバックエンドでの検証に使用する生の初期化データ文字列
initDataUnsafeobject解析済みの初期化データ(ユーザー、チャットなど)— 検証されていないため、クライアント側では信頼しないでください
versionstringSDK のバージョン(例:"8.0")
platformstringプラットフォーム識別子("android"、"ios"、"tdesktop"、"web"、"unknown")
colorSchemestring"light" または "dark"
themeParamsobjectキーと値のペアで表される現在のテーマカラー
isExpandedbooleanミニアプリが全画面の高さまで展開されているかどうか
viewportHeightnumber現在のビューポートの高さ(ピクセル単位)
viewportStableHeightnumber安定したビューポートの高さ(キーボードの表示による変化なし)
headerColorstring現在のヘッダー色(#RRGGBB)
backgroundColorstring現在の背景色(#RRGGBB)
bottomBarColorstring現在のボトムバーの色(#RRGGBB)
isFullscreenboolean全画面モードが有効かどうか
主なサブオブジェクト
サブオブジェクト説明
BackButtonヘッダーの戻るボタンを制御します
MainButtonメインの下部ボタンを制御します
SecondaryButton2つ目の下部ボタンを制御します
SettingsButtonコンテキストメニューの設定項目を制御します
HapticFeedback触覚フィードバック/振動フィードバックを制御します
CloudStorageクラウドのキーと値によるストレージ(最大1024項目、デバイス間で同期)
DeviceStorage永続的なローカルストレージ(最大5 MB、デバイス内のみ)
SecureStorageOS のキーチェーン/キーストアを使用した暗号化ストレージ(最大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-colorbg_colorメインの背景
--tg-theme-text-colortext_colorメインのテキスト
--tg-theme-hint-colorhint_colorセカンダリ/ヒントテキスト
--tg-theme-link-colorlink_colorリンク
--tg-theme-button-colorbutton_colorメインボタンの背景
--tg-theme-button-text-colorbutton_text_colorボタンのテキスト
--tg-theme-secondary-bg-colorsecondary_bg_colorセカンダリの背景(カード、セクション)
--tg-theme-header-bg-colorheader_bg_colorヘッダーの背景
--tg-theme-bottom-bar-bg-colorbottom_bar_bg_colorボトムバーの背景
--tg-theme-accent-text-coloraccent_text_colorアクセントテキスト
--tg-theme-section-bg-colorsection_bg_colorセクション/カードの背景
--tg-theme-section-header-text-colorsection_header_text_colorセクションヘッダーのテキスト
--tg-theme-section-separator-colorsection_separator_color区切り線
--tg-theme-subtitle-text-colorsubtitle_text_colorサブタイトルのテキスト
--tg-theme-destructive-text-colordestructive_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-topTelegram UI のセーフエリア(上)
--tg-content-safe-area-inset-bottomTelegram UI のセーフエリア(下)
--tg-content-safe-area-inset-leftTelegram UI のセーフエリア(左)
--tg-content-safe-area-inset-rightTelegram 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) で削除します。

イベント発生条件
activatedMini App がアクティブ/表示状態になる
deactivatedMini App が非アクティブ/非表示状態になる
themeChangedユーザーが Telegram のテーマを変更した
viewportChangedビューポートの高さが変わった(キーボード、展開)
safeAreaChangedシステムセーフエリアのインセットが変わった
contentSafeAreaChangedTelegram のコンテンツセーフエリアが変わった
mainButtonClickedメインボタンが押された
secondaryButtonClickedセカンダリボタンが押された
backButtonClicked戻るボタンが押された
settingsButtonClicked設定ボタンが押された
invoiceClosed支払い請求書が閉じられた
popupClosedポップアップが閉じられた
qrTextReceivedQR コードがスキャンされた
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 公式デザイン原則です。

基本原則
  1. モバイルファースト:すべての要素はレスポンシブで、モバイルのビューポートに最適化する必要があります
  2. ネイティブな操作感:既存の Telegram UI コンポーネントのスタイルと動作を再現します
  3. 滑らかなアニメーション:すべてのアニメーションで 60fps を目指します
  4. アクセシビリティ:すべての入力欄と画像にラベルを設定します
  5. テーマへの適応:すべての色に Telegram の CSS 変数を使用します
  6. セーフエリアへの配慮:特に全画面モードでは、セーフエリアを考慮します
  7. パフォーマンスへの配慮: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. テストとデバッグ

開発環境
  1. 開発サーバーを localhost で起動します
  2. HTTPS トンネル経由で公開します:ngrok http 3000(または Cloudflare Tunnel、localtunnel など)
  3. BotFather を設定します:
    • ボットを作成/選択します
    • /setmenubutton または /newapp で Web App URL を設定します
  4. Telegram で開いてテストします
デバッグのヒント
プラットフォーム方法
AndroidAndroid の開発者向け設定で WebView のデバッグを有効にする → chrome://inspect
iOSSafari → 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" }));

コメント