#76「Webアプリをスマホでもキレイにしよう」
佐賀県唐津市、合同会社パーチのやすけんです。
GAS 100本ノック、第76回は「Webアプリのレスポンシブ対応」です。
前回 #75 で経費申請Webアプリを作ったとき、スマホで開いたら画面が小さくて「これは使えない……」と凹みました。GASのWebアプリは意外とスマホ対応に落とし穴が多く、PCで動いていてもモバイルで見にくいことがよくあります。

今回はその落とし穴をすべて踏み越えて、タスク管理アプリをモバイル・タブレット・PCで崩れない画面にしてみました。
今回のお題
既存のWebアプリにレスポンシブ対応を追加し、モバイル・タブレット・PCで崩れない画面を実現する。
達成条件は4つです。
meta viewport タグの正しい付与
CSS `@media (max-width: 768px)` でレイアウト切り替え
テーブル表示をモバイルではカード型UIに変換
ローディングスピナーでUX改善
最初にハマったこと: metaタグをHTMLに書いても効かない
GASのWebアプリをスマホで開いたとき、ページ全体が縮小されて文字が豆粒になる——これが最初にぶつかった壁です。
原因を調べたら、なんと GASのHTMLファイルに直接書いた `<meta>` タグは無視される という仕様でした。
<!-- これはGASでは効かない -->
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>GASはHTML出力時にヘッダーを独自に管理しているため、HTMLファイル内のmetaタグが捨てられてしまうのです。
正しい解決策: addMetaTag() をサーバー側で呼ぶ
`doGet()` の中で `HtmlOutput.addMetaTag()` を使うのが正解です。
function doGet(e) {
const output = HtmlService.createTemplateFromFile('index').evaluate();
output
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0, minimum-scale=1.0')
.setTitle('タスク管理 #76');
return output;
}`minimum-scale=1.0` を付けると、ページを最小まで縮小したときに1倍以下にならないよう制限できます。これだけでスマホ縮小問題は解消しました。
テーブル → カード切り替えの実装
GASのWebアプリでよくあるUIパターンは、データをテーブルで表示することです。ところがテーブルはスマホ画面では横スクロールが発生して使いにくい。今回は `@media (max-width: 768px)` を境に、テーブルを非表示にしてカード型UIに切り替えました。
CSSはこんな構造です。
/* PC: テーブルを表示、カードを隠す */
.task-table { /* 通常通り */ }
.task-cards { display: none; }
/* モバイル: テーブルを隠して、カードを表示 */
@media (max-width: 768px) {
.task-table { display: none; }
.task-cards { display: block; }
}HTMLは同じデータを2種類の形でレンダリングしておき、CSSで切り替えるだけです。Vue.jsを使っているので `v-for` を2箇所書くだけで済みました。
<!-- PC: テーブルビュー -->
<table class="task-table">
<tr v-for="task in filteredTasks" :key="task.id">...</tr>
</table>
<!-- モバイル: カードビュー -->
<div class="task-cards">
<div v-for="task in filteredTasks" :key="task.id" class="task-card">...</div>
</div>モバイルのカードUI: 優先度をボーダーカラーで表現
カード型UIで一工夫したのが、優先度を左のボーダーカラーで視覚化したことです。
.task-card.priority-高 { border-left: 4px solid #ef4444; }
.task-card.priority-中 { border-left: 4px solid #f59e0b; }
.task-card.priority-低 { border-left: 4px solid #10b981; }Vue.jsで `:class="'priority-' + task.priority"` のように動的にクラスを付与するだけで、データから見た目が自動的に変わります。テーブルでは文字バッジで表現していた情報を、カードでは色でも伝えられます。
ローディングスピナーの実装
データ取得中は何も表示されないとユーザーが「壊れた?」と思ってしまいます。Vue.jsの `ref(true)` で `loading` フラグを管理し、取得完了後に `false` にしています。
const loading = ref(true);
function loadTasks() {
loading.value = true;
google.script.run
.withSuccessHandler(result => {
tasks.value = result;
loading.value = false;
})
.withFailureHandler(() => {
loading.value = false;
showToast('読み込みに失敗しました');
})
.getTasks();
}HTMLでは `v-if="loading"` でスピナーを表示します。
<div v-if="loading" class="spinner-wrap">
<div class="spinner"></div>
</div>スピナー自体はCSSアニメーションだけで実現しています。外部ライブラリ不要でシンプルに実装できます。
.spinner {
width: 36px;
height: 36px;
border: 3px solid #d1d5db;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
はまった点
フォームのレイアウト崩れ
PCでは1行に並べていた「テキスト入力 + 選択 + ボタン」のフォームが、スマホでは狭すぎてボタンが押せない状態になっていました。
解決は `flex-wrap: wrap` でフォールバックさせることです。テキスト入力に `flex-basis: 100%` を付けて最初の行を占有させ、選択とボタンを2行目に送りました。
@media (max-width: 768px) {
.form-row { flex-wrap: wrap; }
.form-row input[type="text"] { flex-basis: 100%; }
.form-row select, .btn { flex: 1; }
}CSSのスコープはiframe内
GASのWebアプリはiframe内に展開されます。そのため外部ページのCSSは一切届きません。CSSフレームワークを使う場合は必ずindex.html内でCDNを読み込む必要があります。`Bootstrap` を使おうとして親ページのCSSを当てようとしてしばらく悩みました。
まとめ
今回学んだこと
GASはHTMLに直書きのmetaタグを無視する → `doGet()` で `addMetaTag()` を呼ぶのが必須
テーブル → カード変換 は `@media` + `display: none/block` の切り替えで実現できる
Vue.jsの `v-if` + `loading` フラグ でローディングスピナーが簡単に実装できる
GASのWebアプリはiframe内 なので外部CSSは届かない、全スタイルを自己完結させること
モバイル対応はアフターとして後付けするより、最初から設計に組み込んでおくほうが圧倒的に楽です。次回 #77 は「WebアプリにAIチャットを組み込もう」に挑戦します!
GAS 100本ノック #76「Webアプリをスマホでもキレイにしよう」に挑戦!
GASではHTMLに直書きのmetaタグが無視されるという落とし穴を踏んでハマりました。addMetaTag()をdoGet()で呼ぶのが正解。テーブル→カード切り替えとCSSスピナーも実装してモバイル対応完了✨
