MENU

GAS×HTML|カスタムUIを作る方法【ダイアログ・サイドバー・Webアプリ】

GAS×HTML|カスタムUIを作る方法【ダイアログ・サイドバー・Webアプリ】

GASには「HtmlService」という機能があり、HTMLとCSSを使ってカスタムUIを作成できます。これを使えば、ダイアログ、サイドバー、Webアプリなど、見た目も使い勝手も良いツールが作れます。


目次

GASでHTMLを使う意味とメリット

なぜHTMLが必要なのか

GAS単体でもPromptやAlertで簡単な入出力はできますが、限界があります:

方法 できること 限界
Browser.inputBox() 1つの値を入力 複数入力不可
Browser.msgBox() メッセージ表示 選択肢は「はい/いいえ」のみ
SpreadsheetApp.getUi().alert() 警告表示 カスタマイズ不可

HTMLを使えば、これらの制限を突破できます。

HTMLで実現できること


【カスタムダイアログ】
✓ 複数の入力フォーム
✓ ドロップダウン選択
✓ 日付ピッカー
✓ ファイルアップロード

【サイドバー】
✓ 常駐型のツールパネル
✓ ナビゲーションメニュー
✓ リアルタイム情報表示

【Webアプリ】
✓ URL共有でアクセス可能
✓ スプレッドシートなしで動作
✓ スマホからも利用可能

HtmlService の基本

HtmlService のメソッド一覧

GASでHTMLを扱う主要なメソッドです:

メソッド 用途 戻り値
createHtmlOutput(html) HTML文字列からページ作成 HtmlOutput
createHtmlOutputFromFile(filename) HTMLファイルから作成 HtmlOutput
createTemplate(html) テンプレート作成(文字列) HtmlTemplate
createTemplateFromFile(filename) テンプレート作成(ファイル) HtmlTemplate

HtmlOutput のメソッド

メソッド 用途
.setTitle(title) ページタイトル設定
.setWidth(width) 幅設定(ダイアログ用)
.setHeight(height) 高さ設定(ダイアログ用)
.append(html) HTMLを追記
.getContent() HTML文字列を取得

最小構成のサンプル


// Code.gs
function showSimpleDialog() {
  const html = HtmlService.createHtmlOutput('<h1>Hello, GAS!</h1>')
    .setWidth(300)
    .setHeight(200);

  SpreadsheetApp.getUi().showModalDialog(html, 'サンプルダイアログ');
}

このコードを実行すると、「Hello, GAS!」と表示されるダイアログが開きます。


HTMLファイルの作り方

プロジェクトにHTMLファイルを追加

  • GASエディタで「ファイル」→+をクリック
  • HTMLを選択
  • ファイル名を入力(例: dialog
  • HTMLを記述

ファイル構成の基本パターン


プロジェクト構成:
├── Code.gs        ← GASコード
├── dialog.html    ← ダイアログ用HTML
├── sidebar.html   ← サイドバー用HTML
└── style.html     ← 共通CSS(インクルード用)

実践例① カスタムダイアログの作成

入力フォーム付きダイアログ

複数の入力欄を持つダイアログを作成します。

dialog.html


<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body {
      font-family: 'Helvetica Neue', Arial, sans-serif;
      padding: 20px;
      background: #f5f5f5;
    }
    .form-group {
      margin-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
      color: #333;
    }
    input, select {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      box-sizing: border-box;
      font-size: 14px;
    }
    input:focus, select:focus {
      outline: none;
      border-color: #4285f4;
      box-shadow: 0 0 5px rgba(66, 133, 244, 0.3);
    }
    .btn {
      padding: 12px 24px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      margin-right: 10px;
    }
    .btn-primary {
      background: #4285f4;
      color: white;
    }
    .btn-primary:hover {
      background: #3367d6;
    }
    .btn-secondary {
      background: #f1f3f4;
      color: #5f6368;
    }
    .btn-secondary:hover {
      background: #e8eaed;
    }
    .button-group {
      margin-top: 20px;
      text-align: right;
    }
  </style>
</head>
<body>
  <div class="form-group">
    <label for="name">名前</label>
    <input type="text" id="name" placeholder="山田太郎">
  </div>

  <div class="form-group">
    <label for="email">メールアドレス</label>
    <input type="email" id="email" placeholder="example@email.com">
  </div>

  <div class="form-group">
    <label for="department">部署</label>
    <select id="department">
      <option value="">選択してください</option>
      <option value="営業部">営業部</option>
      <option value="開発部">開発部</option>
      <option value="人事部">人事部</option>
      <option value="総務部">総務部</option>
    </select>
  </div>

  <div class="button-group">
    <button class="btn btn-secondary" onclick="google.script.host.close()">
      キャンセル
    </button>
    <button class="btn btn-primary" onclick="submitForm()">
      登録
    </button>
  </div>

  <script>
    function submitForm() {
      const data = {
        name: document.getElementById('name').value,
        email: document.getElementById('email').value,
        department: document.getElementById('department').value
      };

      // 入力チェック
      if (!data.name || !data.email || !data.department) {
        alert('すべての項目を入力してください');
        return;
      }

      // GASの関数を呼び出し
      google.script.run
        .withSuccessHandler(onSuccess)
        .withFailureHandler(onError)
        .processFormData(data);
    }

    function onSuccess(result) {
      alert(result);
      google.script.host.close();
    }

    function onError(error) {
      alert('エラー: ' + error.message);
    }
  </script>
</body>
</html>

Code.gs


// メニューに追加
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('カスタムメニュー')
    .addItem('登録フォームを開く', 'showRegistrationDialog')
    .addToUi();
}

// ダイアログを表示
function showRegistrationDialog() {
  const html = HtmlService.createHtmlOutputFromFile('dialog')
    .setWidth(400)
    .setHeight(350);

  SpreadsheetApp.getUi().showModalDialog(html, '社員登録フォーム');
}

// フォームデータを処理
function processFormData(data) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  // スプレッドシートに追記
  sheet.appendRow([
    new Date(),
    data.name,
    data.email,
    data.department
  ]);

  return '登録が完了しました: ' + data.name + 'さん';
}

ポイント解説


【HTML→GAS通信】
google.script.run.関数名(引数)
- .withSuccessHandler(): 成功時のコールバック
- .withFailureHandler(): 失敗時のコールバック

【ダイアログを閉じる】
google.script.host.close()

実践例② サイドバーUIの作成

常駐型ツールパネル

スプレッドシートの横に常に表示されるサイドバーを作成します。

sidebar.html


<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body {
      font-family: 'Helvetica Neue', Arial, sans-serif;
      padding: 15px;
      margin: 0;
    }
    h3 {
      color: #1a73e8;
      border-bottom: 2px solid #1a73e8;
      padding-bottom: 10px;
      margin-top: 0;
    }
    .tool-button {
      display: block;
      width: 100%;
      padding: 12px;
      margin-bottom: 10px;
      background: #f8f9fa;
      border: 1px solid #dadce0;
      border-radius: 8px;
      cursor: pointer;
      text-align: left;
      font-size: 14px;
      transition: all 0.2s;
    }
    .tool-button:hover {
      background: #e8f0fe;
      border-color: #1a73e8;
    }
    .tool-button .icon {
      margin-right: 10px;
    }
    .status {
      margin-top: 20px;
      padding: 15px;
      background: #e8f5e9;
      border-radius: 8px;
      font-size: 13px;
      color: #1b5e20;
    }
    .status.error {
      background: #ffebee;
      color: #c62828;
    }
    .loading {
      display: none;
      text-align: center;
      padding: 20px;
      color: #666;
    }
    .section {
      margin-bottom: 25px;
    }
  </style>
</head>
<body>
  <h3>🛠️ ツールパネル</h3>

  <div class="section">
    <button class="tool-button" onclick="runTool('formatCells')">
      <span class="icon">🎨</span>セルの書式を整える
    </button>

    <button class="tool-button" onclick="runTool('removeEmpty')">
      <span class="icon">🗑️</span>空白行を削除
    </button>

    <button class="tool-button" onclick="runTool('sortData')">
      <span class="icon">📊</span>データを並び替え
    </button>

    <button class="tool-button" onclick="runTool('createBackup')">
      <span class="icon">💾</span>バックアップを作成
    </button>
  </div>

  <div class="loading" id="loading">
    ⏳ 処理中...
  </div>

  <div class="status" id="status" style="display: none;"></div>

  <script>
    function runTool(toolName) {
      // ローディング表示
      document.getElementById('loading').style.display = 'block';
      document.getElementById('status').style.display = 'none';

      // GAS関数を呼び出し
      google.script.run
        .withSuccessHandler(showSuccess)
        .withFailureHandler(showError)
        .executeTool(toolName);
    }

    function showSuccess(message) {
      document.getElementById('loading').style.display = 'none';
      const status = document.getElementById('status');
      status.className = 'status';
      status.innerHTML = '✅ ' + message;
      status.style.display = 'block';
    }

    function showError(error) {
      document.getElementById('loading').style.display = 'none';
      const status = document.getElementById('status');
      status.className = 'status error';
      status.innerHTML = '❌ エラー: ' + error.message;
      status.style.display = 'block';
    }
  </script>
</body>
</html>

Code.gs


// メニューに追加
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('ツール')
    .addItem('サイドバーを開く', 'showSidebar')
    .addToUi();
}

// サイドバーを表示
function showSidebar() {
  const html = HtmlService.createHtmlOutputFromFile('sidebar')
    .setTitle('ツールパネル');

  SpreadsheetApp.getUi().showSidebar(html);
}

// ツール実行
function executeTool(toolName) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();

  switch (toolName) {
    case 'formatCells':
      return formatCells(sheet);
    case 'removeEmpty':
      return removeEmptyRows(sheet);
    case 'sortData':
      return sortData(sheet);
    case 'createBackup':
      return createBackup();
    default:
      throw new Error('不明なツール: ' + toolName);
  }
}

// セルの書式を整える
function formatCells(sheet) {
  const range = sheet.getDataRange();

  // ヘッダー行を太字に
  sheet.getRange(1, 1, 1, range.getLastColumn())
    .setFontWeight('bold')
    .setBackground('#e8f0fe');

  // 罫線を追加
  range.setBorder(true, true, true, true, true, true);

  // 列幅を自動調整
  for (let i = 1; i <= range.getLastColumn(); i++) {
    sheet.autoResizeColumn(i);
  }

  return '書式を整えました';
}

// 空白行を削除
function removeEmptyRows(sheet) {
  const data = sheet.getDataRange().getValues();
  let deletedCount = 0;

  // 下から上に向かって削除(行番号がずれないように)
  for (let i = data.length - 1; i >= 0; i--) {
    const isEmpty = data[i].every(cell => cell === '' || cell === null);
    if (isEmpty) {
      sheet.deleteRow(i + 1);
      deletedCount++;
    }
  }

  return deletedCount + '行の空白行を削除しました';
}

// データを並び替え
function sortData(sheet) {
  const range = sheet.getDataRange();

  // ヘッダー行を除いて1列目でソート
  if (range.getNumRows() > 1) {
    sheet.getRange(2, 1, range.getNumRows() - 1, range.getNumColumns())
      .sort(1);
  }

  return 'データを並び替えました';
}

// バックアップを作成
function createBackup() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const backupName = ss.getName() + '_backup_' +
    Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyyMMdd_HHmmss');

  const backup = ss.copy(backupName);

  return 'バックアップを作成しました: ' + backupName;
}

実践例③ Webアプリとして公開

独立したWebアプリ

スプレッドシートなしで動作する、独立したWebアプリを作成します。

webapp.html


<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    * {
      box-sizing: border-box;
    }
    body {
      font-family: 'Helvetica Neue', Arial, sans-serif;
      max-width: 600px;
      margin: 0 auto;
      padding: 20px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
    }
    .container {
      background: white;
      border-radius: 16px;
      padding: 30px;
      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
    }
    h1 {
      color: #333;
      text-align: center;
      margin-bottom: 30px;
    }
    .form-group {
      margin-bottom: 20px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
      color: #555;
    }
    input, textarea {
      width: 100%;
      padding: 12px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      font-size: 16px;
      transition: border-color 0.3s;
    }
    input:focus, textarea:focus {
      outline: none;
      border-color: #667eea;
    }
    textarea {
      height: 120px;
      resize: vertical;
    }
    .btn {
      width: 100%;
      padding: 15px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 18px;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
    }
    .btn:disabled {
      opacity: 0.6;
      cursor: not-allowed;
      transform: none;
    }
    .message {
      margin-top: 20px;
      padding: 15px;
      border-radius: 8px;
      text-align: center;
      display: none;
    }
    .message.success {
      background: #d4edda;
      color: #155724;
    }
    .message.error {
      background: #f8d7da;
      color: #721c24;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>📝 お問い合わせフォーム</h1>

    <form id="contactForm">
      <div class="form-group">
        <label for="name">お名前 *</label>
        <input type="text" id="name" required>
      </div>

      <div class="form-group">
        <label for="email">メールアドレス *</label>
        <input type="email" id="email" required>
      </div>

      <div class="form-group">
        <label for="subject">件名</label>
        <input type="text" id="subject">
      </div>

      <div class="form-group">
        <label for="message">お問い合わせ内容 *</label>
        <textarea id="message" required></textarea>
      </div>

      <button type="submit" class="btn" id="submitBtn">送信する</button>
    </form>

    <div class="message" id="resultMessage"></div>
  </div>

  <script>
    document.getElementById('contactForm').addEventListener('submit', function(e) {
      e.preventDefault();

      const btn = document.getElementById('submitBtn');
      btn.disabled = true;
      btn.textContent = '送信中...';

      const formData = {
        name: document.getElementById('name').value,
        email: document.getElementById('email').value,
        subject: document.getElementById('subject').value || '(件名なし)',
        message: document.getElementById('message').value,
        timestamp: new Date().toLocaleString('ja-JP')
      };

      google.script.run
        .withSuccessHandler(onSuccess)
        .withFailureHandler(onError)
        .submitContactForm(formData);
    });

    function onSuccess(result) {
      const msg = document.getElementById('resultMessage');
      msg.className = 'message success';
      msg.textContent = '✅ ' + result;
      msg.style.display = 'block';

      document.getElementById('contactForm').reset();
      document.getElementById('submitBtn').disabled = false;
      document.getElementById('submitBtn').textContent = '送信する';
    }

    function onError(error) {
      const msg = document.getElementById('resultMessage');
      msg.className = 'message error';
      msg.textContent = '❌ エラー: ' + error.message;
      msg.style.display = 'block';

      document.getElementById('submitBtn').disabled = false;
      document.getElementById('submitBtn').textContent = '送信する';
    }
  </script>
</body>
</html>

Code.gs


// Webアプリのエントリポイント(必須)
function doGet() {
  return HtmlService.createHtmlOutputFromFile('webapp')
    .setTitle('お問い合わせフォーム')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// フォーム送信処理
function submitContactForm(formData) {
  // スプレッドシートに保存
  const ss = SpreadsheetApp.openById('YOUR_SPREADSHEET_ID'); // ← IDを設定
  const sheet = ss.getSheetByName('問い合わせ') || ss.insertSheet('問い合わせ');

  // ヘッダーがなければ追加
  if (sheet.getLastRow() === 0) {
    sheet.appendRow(['受付日時', '名前', 'メール', '件名', '内容', 'ステータス']);
    sheet.getRange(1, 1, 1, 6).setFontWeight('bold').setBackground('#e8f0fe');
  }

  // データを追記
  sheet.appendRow([
    formData.timestamp,
    formData.name,
    formData.email,
    formData.subject,
    formData.message,
    '未対応'
  ]);

  // 通知メールを送信(オプション)
  // sendNotificationEmail(formData);

  return 'お問い合わせを受け付けました。ありがとうございます。';
}

// 通知メール送信(オプション)
function sendNotificationEmail(formData) {
  const adminEmail = 'admin@example.com';

  const subject = '【お問い合わせ】' + formData.subject;
  const body = `
新しいお問い合わせがありました。

■ 名前: ${formData.name}
■ メール: ${formData.email}
■ 件名: ${formData.subject}
■ 内容:
${formData.message}

■ 受付日時: ${formData.timestamp}
  `;

  GmailApp.sendEmail(adminEmail, subject, body);
}

Webアプリのデプロイ手順


【手順】
1. GASエディタで「デプロイ」→「新しいデプロイ」
2. 種類で「ウェブアプリ」を選択
3. 説明を入力(例: お問い合わせフォーム v1.0)
4. 実行ユーザー: 「自分」
5. アクセス権: 「全員」(公開する場合)
6. 「デプロイ」をクリック
7. URLが発行される

【更新時】
1. 「デプロイ」→「デプロイを管理」
2. 「新しいデプロイ」または既存を編集

HTMLテンプレートの使い方

テンプレート記法(

GASの値をHTMLに埋め込むには、テンプレート記法を使います。

template.html


<!DOCTYPE html>
<html>
<head>
  <base target="_top">
</head>
<body>
  <!-- 値を出力(HTMLエスケープあり) -->
  <p>こんにちは、<?= userName ?>さん</p>

  <!-- 値を出力(HTMLエスケープなし、HTMLタグを含む場合) -->
  <div><?!= htmlContent ?></div>

  <!-- 繰り返し処理 -->
  <ul>
    <? for (var i = 0; i < items.length; i++) { ?>
      <li><?= items[i] ?></li>
    <? } ?>
  </ul>

  <!-- 条件分岐 -->
  <? if (isAdmin) { ?>
    <button>管理者メニュー</button>
  <? } ?>
</body>
</html>

Code.gs


function showTemplateDialog() {
  const template = HtmlService.createTemplateFromFile('template');

  // テンプレートに値を渡す
  template.userName = '山田太郎';
  template.htmlContent = '<strong>太字テキスト</strong>';
  template.items = ['りんご', 'みかん', 'バナナ'];
  template.isAdmin = true;

  // HTMLに変換
  const html = template.evaluate()
    .setWidth(400)
    .setHeight(300);

  SpreadsheetApp.getUi().showModalDialog(html, 'テンプレート例');
}

テンプレート記法まとめ

記法 用途
値を出力(エスケープあり)
値を出力(エスケープなし)
GASコードを実行

ファイル分割とインクルード

CSSやJavaScriptを別ファイルに分けて読み込む方法:

style.html


<style>
  .container { max-width: 600px; margin: 0 auto; }
  .btn { padding: 10px 20px; background: #4285f4; color: white; }
</style>

main.html


<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <!-- CSSをインクルード -->
  <?!= HtmlService.createHtmlOutputFromFile('style').getContent(); ?>
</head>
<body>
  <div class="container">
    <button class="btn">ボタン</button>
  </div>
</body>
</html>

Code.gs


// インクルード用関数
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

テンプレートからは以下のように呼び出し:


<?!= include('style'); ?>

CSSでスタイリング

Google風マテリアルデザイン


<style>
  /* Google Material Design風 */
  :root {
    --primary: #1a73e8;
    --primary-dark: #1557b0;
    --surface: #ffffff;
    --background: #f8f9fa;
    --text-primary: #202124;
    --text-secondary: #5f6368;
    --border: #dadce0;
    --shadow: 0 1px 3px rgba(60, 64, 67, 0.3);
  }

  body {
    font-family: 'Google Sans', 'Roboto', sans-serif;
    background: var(--background);
    color: var(--text-primary);
    margin: 0;
    padding: 16px;
  }

  /* カード */
  .card {
    background: var(--surface);
    border-radius: 8px;
    box-shadow: var(--shadow);
    padding: 24px;
    margin-bottom: 16px;
  }

  /* ボタン */
  .btn {
    display: inline-flex;
    align-items: center;
    padding: 10px 24px;
    border: none;
    border-radius: 4px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
  }

  .btn-primary {
    background: var(--primary);
    color: white;
  }

  .btn-primary:hover {
    background: var(--primary-dark);
    box-shadow: 0 1px 3px rgba(26, 115, 232, 0.5);
  }

  .btn-outlined {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--primary);
  }

  .btn-outlined:hover {
    background: rgba(26, 115, 232, 0.04);
  }

  /* 入力フォーム */
  .input {
    width: 100%;
    padding: 12px 16px;
    border: 1px solid var(--border);
    border-radius: 4px;
    font-size: 14px;
    transition: border-color 0.2s;
  }

  .input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
  }
</style>

よくあるエラーと対処法

エラー1: google.script.run が動かない


【原因】
- HTMLファイルに <base target="_top"> がない
- 関数名のスペルミス
- GAS側の関数がグローバルでない

【対処】
1. <head>内に <base target="_top"> を追加
2. 関数名を確認
3. function が最上位レベルにあるか確認

エラー2: ダイアログのサイズが反映されない


【原因】
setWidth/setHeight がevaluate()の後に呼ばれている

【正しい順序】
const html = HtmlService.createTemplateFromFile('dialog')
  .evaluate()                    // ← 先にevaluate
  .setWidth(400)                 // ← その後にサイズ設定
  .setHeight(300);

エラー3: 日本語が文字化けする


【対処】
HTMLファイルの先頭に以下を追加:
<meta charset="UTF-8">

まとめ

GASのHTML機能を使えば、見た目も使い勝手も良いカスタムUIが作成できます。

今日から使えるテクニック

用途 メソッド 特徴
ダイアログ showModalDialog() 入力フォーム、確認画面
サイドバー showSidebar() 常駐ツール、ナビゲーション
Webアプリ doGet() URL共有、外部公開

次のステップ

この記事のコードを参考に、ぜひオリジナルのUIを作ってみてください。


よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次