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, 'テンプレート例');
}
テンプレート記法まとめ
| 記法 | 用途 | 例 |
|---|---|---|
= ?> |
値を出力(エスケープあり) | = name ?> |
!= ?> |
値を出力(エスケープなし) | != htmlTag ?> |
?> |
GASコードを実行 | for (...) { ?> |
ファイル分割とインクルード
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を作ってみてください。
コメント