Googleドライブの画像を、GAS(Google Apps Script)のWebアプリで一覧表示する作例です。
GASはGoogleサービスを操作するスクリプトです。
本人限定の練習用データを対象とし、現行コードの実サービス上での動作確認は未完了です。
- Googleの標準サービスを利用できる(保存容量・契約プラン・GASの割り当ての範囲内)
- Webアプリの公開範囲を設定できる(この例は本人限定で試す)
- GASとHTMLのコードを編集して、項目や表示方法を変更できる
- スプレッドシート上でデータを編集・管理しやすい
- Googleドライブ上の画像を参照できる(空き容量と画像の閲覧権限の確認が必要)

ギャラリー表示機能の概要
掲載画像は既存の画面例で、今回の改訂コードを実行して撮影したものではありません。
先に画像・管理データを用意し、5ファイルの保存後にメニューから画像一覧を取得します。
1. Googleドライブの設定
自分で閲覧できる専用フォルダを作り、まず機密情報を含まない画像2枚で試します。

2. スプレッドシートへのデータ入力
スプレッドシートをデータベースとして利用します。
「管理シート」のA1:G1に、id、file_name、item_name、image_path、tag、rare、typeを順に入力します。
2行目以降へデータを入れ、idは1、2のような重複しない数値にしてください。
image_pathには後述の画像URLを入れます。
tag・rare・typeの複数値は半角カンマで区切り、値そのものにカンマは使いません。

「画像パスの取得」という専用シートを用意し、カスタムメニューでファイル名・画像URL・フォルダパスを出力します。
取得したURLとファイル名を管理シートのimage_pathとfile_nameへ転記し、item_nameも入力してください。
取得用シートのA〜C列は再実行で更新するため、手入力データは置かず、先にコピーを保存します。

3. Webアプリ上でのギャラリー表示
GASで構築したWebアプリがスプレッドシートのデータを読み込み、ギャラリー形式で画面に描画します。

画像カードにカーソルを合わせると詳細情報が表示されます。

絞り込み条件を指定して検索ボタンを押すと、該当する画像のみに絞り込まれます。

サムネイル付きギャラリーの実装手順
スプレッドシートのメニュー「拡張機能」から「Apps Script」を選択し、スクリプトエディタを起動します。
プロジェクトは以下の5つのファイルで構成します。

| main.gs | バックエンドの処理実装 |
|---|---|
| index.html | 画面の基本HTML構造 |
| css.html | レイアウトやホバー演出のスタイル |
| js.html | 表示制御や絞り込み検索のスクリプト |
| plugin.html | 外部ライブラリの読み込み設定 |
以下は実装の詳細なコードと解説です。
※本コードはベースサンプルです。
この例の公開範囲は本人限定です。
自分として実行するWebアプリは、所有者の権限で管理データを読み出します。
管理シートの7列すべてをブラウザーへ返すため、画面の絞り込みでは閲覧権限を制限できません。
管理データに機密情報を入れず、他人へ公開する場合は閲覧者ごとの認可を別途実装してください。
Googleスプレッドシートの関連テクニックは、まとめ記事でも紹介しています。

5つのファイルにコードを記述したあと、ウェブアプリとしてデプロイして動作を確認する手順で進めます。
plugin.htmlの実装
外部ライブラリはCDN(外部のファイル配信サービス)から掲載版を読み込みます。
最新の保守・脆弱性状況と相互互換性は未検証です。
業務利用前に採用版を確認してください。
<!-- jQuery full -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<!-- iziModal -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.6.0/css/iziModal.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.6.0/js/iziModal.min.js"></script>
<!-- Underscore / lodash -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore-min.js"></script>
<!-- Bootstrap & select -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.10/dist/css/bootstrap-select.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js"></script>
<!-- twbs-pagination -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twbs-pagination/1.4.2/jquery.twbsPagination.min.js"></script>css.htmlの実装
デザインとスタイルの設定です。
画像ホバー時にメタ情報を表示するスタイルなどを指定しています。
<style>
/* ===================== 共通リセット ===================== */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* ===================== 操作エリア ===================== */
#header { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }
.selectpicker { width: 200px; }
/* ===== 画像グリッド ===== */
#btn-search {
background-color: #333;
border-color: #333;
}
.btn.btn-info {
background-color: #28a745;
border-color: #28a745;
}
/* ===== 画像グリッド ===== */
#grid{
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 20px;
list-style: none;
padding: 0;
}
/* 画面幅に応じて列数を自動調整 */
@media (max-width: 1280px){ #grid{ grid-template-columns: repeat(4, minmax(0,1fr)); } }
@media (max-width: 992px) { #grid{ grid-template-columns: repeat(3, minmax(0,1fr)); } }
@media (max-width: 768px) { #grid{ grid-template-columns: repeat(2, minmax(0,1fr)); } }
/* ===== カード ===== */
#grid .list_item{ position: relative; }
#grid img.card{
width: 100%;
height: auto;
object-fit: contain;
transition: filter .5s;
}
/* ===== ホバー時のエフェクト ===== */
#grid .list_item:hover img.card{ filter: brightness(50%); }
#grid .list_item:hover::after {
content: attr(data-alt) '\A' attr(data-name) '\A ID : ' attr(data-id);
white-space: pre-wrap;
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 90%;
color: #fff;
pointer-events: none;
}
/* ===================== モーダル画像 ===================== */
.iziModal{
position: fixed !important; /* 念のため固定 */
left: 50% !important;
top : 50% !important;
transform: translate(-50%,-50%) !important;
margin: 0 !important;
max-width : 90vw !important; /* 画面幅 90% まで拡大可 */
width : auto !important; /* 幅は内容に合わせて伸縮 */
}
.iziModal-content {
padding: 1em!important;
}
.modal_card {
width: 100%;
}
</style>main.gsの実装
スプレッドシートやドライブとの連携を処理するサーバーサイドコードです。
コード内の定数定義に以下の設定値を指定します。
- 対象スプレッドシートのID
- 画像を格納しているGoogleドライブフォルダのID
- 画像パスを出力するシート名
- メタデータを管理しているシート名
各項目をお手元の環境に合わせて設定してください。
シート上から各機能を呼び出せるよう、カスタムメニューも追加しています。

「ギャラリー」シートを作り、デプロイ後に発行された/execで終わるURLをA1へ入力します。
画像はPNG・JPEG・GIF・WebPのみを収集します。
走査はルートを含む10階層・100フォルダ・1000ファイル、対象画像500件までのサンプルです。
これらはコード内の制限で、Google Drive全体の上限ではありません。

/* =====================================================================
* 定数定義
* ===================================================================*/
const SS_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const FOLDER_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxx';
const SHEET_IMG_PATH = '画像パスの取得';
const SHEET_DATA_MAIN = '管理シート';
/* =====================================================================
* 画像ファイル一覧をスプレッドシートへ書き込む
* ===================================================================*/
function exportDriveImages_() {
const rootFolder = DriveApp.getFolderById(FOLDER_ID);
const list = [];
getFilesRecursively_(rootFolder, rootFolder.getName(), list, {folders: 0, files: 0}, 0);
if (SHEET_IMG_PATH === SHEET_DATA_MAIN) throw new Error("取得用と管理用のシートは分けてください。");
const sheet = getSheet_(SHEET_IMG_PATH);
const neededRows = list.length + 1;
if (sheet.getMaxRows() < neededRows) sheet.insertRowsAfter(sheet.getMaxRows(), neededRows - sheet.getMaxRows());
sheet.getRange(1, 1, Math.max(sheet.getLastRow(), 1), 3).clearContent();
sheet.getRange(1, 1, 1, 3).setValues([['ファイル名', '画像URL', 'フォルダパス']]);
if (list.length) sheet.getRange(2, 1, list.length, 3).setValues(list.map(row => row.map(value => /^[=+@-]/.test(value) ? "'" + value : value)));
}
function getFilesRecursively_(folder, path, list, state, depth) {
if (depth >= 10 || ++state.folders > 100) throw new Error('フォルダ走査の上限です。対象を小さくしてください。');
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
if (++state.files > 1000) throw new Error('ファイル走査は1000件までです。');
if (!['image/png','image/jpeg','image/gif','image/webp'].includes(file.getMimeType())) continue;
if (list.length >= 500) throw new Error('対象画像は500件までです。');
const id = file.getId();
const thumbUrl = `https://drive.google.com/thumbnail?id=${id}`;
list.push([file.getName(), thumbUrl, path]);
}
const folders = folder.getFolders();
while (folders.hasNext()) {
const sub = folders.next();
const subPath = `${path}/${sub.getName()}`;
getFilesRecursively_(sub, subPath, list, state, depth + 1);
}
}
/* =====================================================================
* Web アプリ公開
* ===================================================================*/
function doGet () {
return HtmlService
.createTemplateFromFile('index')
.evaluate();
}
/* 新しいタブで Web アプリを開く(メニュー用)*/
function openViewer_ () {
const url = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('ギャラリー')
.getRange('A1')
.getValue();
if (!/^https:\/\/script\.google\.com\/macros\/s\/[A-Za-z0-9_-]+\/exec$/.test(String(url))) {
throw new Error('ギャラリー!A1にWebアプリのexec URLを設定してください。');
}
const safeUrl = JSON.stringify(String(url));
const script = `
<script>
const opened = window.open(${safeUrl}, '_blank');
if (opened) opened.focus();
google.script.host.close();
</script>`;
SpreadsheetApp.getUi()
.showModalDialog(HtmlService.createHtmlOutput(script), 'CardIllustViewer');
}
/* =====================================================================
* カスタムメニュー
* ===================================================================*/
function onOpen () {
SpreadsheetApp.getUi().createMenu('カスタムメニュー')
.addItem('ギャラリーを表示', 'openViewer_')
.addItem('画像パスの取得', 'exportDriveImages_')
.addToUi();
}
/* =====================================================================
* データ取得 API(HTML から呼び出し)
* ===================================================================*/
function getSSValues () {
return JSON.stringify(readSheetObjects_(SHEET_DATA_MAIN));
}
/* =====================================================================
* 共通ユーティリティ
* ===================================================================*/
/** シート取得 */
function getSheet_ (name) {
const ss = SpreadsheetApp.openById(SS_ID);
const sheet = ss.getSheetByName(name);
if (!sheet) throw new Error('シートがありません: ' + name);
return sheet;
}
/** ヘッダー行をキーに行データをオブジェクト配列化 */
function readSheetObjects_ (sheetName) {
const sheet = getSheet_(sheetName);
if (sheet.getLastRow() > 501) throw new Error('管理データは500件までです。');
const expected = ['id','file_name','item_name','image_path','tag','rare','type'];
const [header, ...rows] = sheet.getRange(1, 1, Math.max(sheet.getLastRow(), 1), 7).getDisplayValues();
if (header.join('|') !== expected.join('|')) throw new Error('管理シートの7列の見出しを確認してください。');
return rows.map(row =>
header.reduce((obj, key, idx) => {
obj[String(key)] = String(row[idx]);
return obj;
}, {}));
}
/** HTML 部品の include_ 用 */
function include_ (file) {
return HtmlService.createHtmlOutputFromFile(file).getContent();
}
index.htmlの実装
ギャラリー画面を描画するメインのHTMLファイルです。
コード内のシートURL指定箇所は、ご自身の環境に合わせて書き換えてください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>画像一覧</title>
<!-- CSS & プラグイン読込 -->
<?!= include_('plugin') ?>
<?!= include_('css') ?>
</head>
<body>
<!-- ===== フィルター・操作エリア ===== -->
<header id="header" class="d-flex flex-wrap gap-2 mb-3">
<select id="filter" class="selectpicker" multiple title="フィルター"
data-actions-box="true" data-selected-text-format="count > 3" size="4"></select>
<select id="select_chara" class="selectpicker" multiple title="アイテム名"
data-actions-box="true" data-selected-text-format="count > 3"></select>
<select id="select_tag" class="selectpicker" multiple title="タグ"
data-live-search="true" data-actions-box="true"
data-live-search-placeholder="Search" data-selected-text-format="count > 3"></select>
<button id="btn-search" class="btn btn-success">検索</button>
<button class="btn btn-info" onclick="window.open('https://docs.google.com/spreadsheets/d/xxxxxxxxx/edit?gid=0#gid=0')">シート</button>
</header>
<!-- ================= ページャ ================= -->
<nav id="pager">
<ul id="pagenation"></ul>
</nav>
<!-- ================= 画像グリッド ================= -->
<main>
<ul id="grid"></ul>
</main>
<!-- ================= 画像モーダル ================= -->
<div class="iziModal"></div>
<!-- ================= JavaScript ================= -->
<?!= include_('js') ?>
</body>
</html>
js.htmlの実装
フロントエンド側の表示制御を行うJavaScriptコードです。
rareとtypeは列ごとに判定し、異なる列の条件は両方を満たす画像に絞ります。
同じ列で複数選んだ場合とタグを複数選んだ場合は、そのどれかに一致する画像が対象です。
<script>
(() => {
/* ------------------------------------------------------------------
* 1. 定数・キャッシュ
* ------------------------------------------------------------------*/
const PER_PAGE = 18;
// jQuery オブジェクトのキャッシュ
const $grid = $('#grid'); // 画像グリッド
const $pager = $('#pagenation'); // ページャ
const $filter = $('#filter');
const $chara = $('#select_chara');
const $tags = $('#select_tag');
const $modal = $('.iziModal');
/* ------------------------------------------------------------------
* 2. 状態変数
* ------------------------------------------------------------------*/
let allData = []; // 管理シートの7列(最大500件)
let listData = []; // 画像URLとアイテム名のあるデータ
let filterKeys = ["rare", "type"];
let chunks = []; // ページング用
let selFilter = [], selChara = [], selTags = [];
/* ------------------------------------------------------------------
* 3. 初期化
* ------------------------------------------------------------------*/
$(async () => {
$('#btn-search').on('click', onSearchClick);
try { initModal(); await loadSheetData(); await buildUI(); }
catch (_) { $grid.empty().append($('<li>').text('初期化に失敗しました。ライブラリの読込み、シートの設定、実行権限を確認してください。')); }
});
/* ------------------------------------------------------------------
* 4. モーダル初期化
* ------------------------------------------------------------------*/
function initModal () {
const $modal = $('.iziModal');
const $grid = $('#grid');
$modal.iziModal({
maxWidth : 0, // 0 = 制限なし(プラグイン推奨値)
maxHeight: '90%', // 高さは 90% に制限
width : null, // 自動
transitionIn: 'fadeIn',
onClosed : () => $modal.iziModal('setContent', '')
});
$grid.on('click', 'img.card', function () {
const full = $(this).data('full');
const alt = this.alt;
const $modal = $('.iziModal');
const img = new Image();
img.onload = function () {
const html = $('<img>', {src: full, alt: alt, class: 'modal_card'})[0].outerHTML;
$modal.iziModal('setContent', html);
$modal.iziModal('open');
};
img.onerror = () => alert('画像を取得できません。閲覧権限とログイン状態を確認してください。');
img.src = full;
});
}
/* ------------------------------------------------------------------
* 5. データ取得
* ------------------------------------------------------------------*/
async function loadSheetData () {
const json = await new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
.getSSValues();
});
allData = JSON.parse(json);
listData = allData.filter(d =>
getThumb(d) && d.item_name
);
}
/* ------------------------------------------------------------------
* 6. UI 構築
* ------------------------------------------------------------------*/
async function buildUI () {
/* プルダウン ---------------- */
const charaSet = uniqCompact(propArray(allData, 'item_name'));
populateSelect($chara, charaSet);
/* タグプルダウン ------------------ */
const tagSet = uniqCompact(
flatMap(listData, d => splitCSV(d.tag))
);
populateSelect($tags, tagSet);
/* 絞り込みキー -------------------- */
refreshFilterSelect();
/* 一覧 & ページング --------------- */
listData = _.sortBy(listData, d => -d.id);
renderPage(listData);
}
/* ------------------------------------------------------------------
* 7. 検索処理
* ------------------------------------------------------------------*/
function onSearchClick () {
selFilter = $filter.val() || [];
selChara = $chara.val() || [];
selTags = $tags.val() || [];
const filtered = listData
.filter(d => !selChara.length || selChara.includes(d.item_name))
.filter(d => filterRow(d, selFilter))
.filter(d => tagMatch(d.tag, selTags));
renderPage(filtered);
}
function filterRow (row, selected) {
if (!selected.length) return true;
return filterKeys.every(key => {
const keyOpts = getColumnValues(key);
const hit = keyOpts.filter(value => selected.includes(JSON.stringify([key, value])));
if (!hit.length) return true;
const rowVals = splitCSV(row[key]);
return rowVals.some(v => hit.includes(v));
});
}
function tagMatch (tagStr, selected) {
if (!selected.length) return true;
const tagArr = splitCSV(tagStr);
return tagArr.some(t => selected.includes(t));
}
/* ------------------------------------------------------------------
* 8. 描画関連
* ------------------------------------------------------------------*/
function renderPage (data) {
const firstPage = setupPagination(data);
renderGrid(firstPage);
}
function renderGrid(data) {
$grid.empty();
if (!data.length) {
$('<li>').text('一致する画像がありません。条件を解除するか、管理シートの画像URLとアイテム名を確認してください。').appendTo($grid);
return;
}
data.forEach(d => {
const $li = $('<li>', {
class: 'list_item',
name : d.file_name,
alt : d.item_name,
'data-id': d.id,
'data-name': d.file_name,
'data-alt': d.item_name
});
const thumb = getThumb(d);
const full = getFull(d);
if (!thumb) return;
$('<img>', {
class : 'card',
src : thumb,
'data-full': full,
alt : d.file_name,
loading : 'lazy'
}).appendTo($li);
$grid.append($li);
});
}
function setupPagination (data) {
if ($pager.data('twbs-pagination')) $pager.twbsPagination('destroy');
if (data.length <= PER_PAGE) {
$pager.hide();
return data;
}
chunks = _.chunk(data, PER_PAGE);
$pager.twbsPagination({
totalPages : chunks.length,
visiblePages: 5,
onPageClick : (_, page) => renderGrid(chunks[page-1])
});
$pager.show();
return chunks[0];
}
/* ------------------------------------------------------------------
* 9. フィルターセレクト生成
* ------------------------------------------------------------------*/
function refreshFilterSelect () {
$filter.empty();
filterKeys.forEach(key => {
const opts = getColumnValues(key);
const $group = $('<optgroup>', { label: key });
opts.forEach(v => $('<option>', { value: JSON.stringify([key, v]), text:v }).appendTo($group));
$group.appendTo($filter);
});
$filter.selectpicker('refresh');
}
/* ------------------------------------------------------------------
* 10. ユーティリティ
* ------------------------------------------------------------------*/
const flatMap = (arr, fn) => [].concat(...arr.map(fn)); // lodash 代替
const uniqCompact = arr => _.uniq(arr.filter(Boolean)); // 空文字除外+重複排除
const propArray = (arr, key) => arr.map(o => o[key]); // プロパティ抽出
const toBool = v => String(v).toLowerCase() === 'true';
const splitCSV = str => String(str || '').split(',').map(v => v.trim()).filter(Boolean);
function imageUrl(d, size) {
try {
const url = new URL(d.image_path);
if (url.origin !== 'https://drive.google.com' || url.pathname !== '/thumbnail') return '';
if (!/^[A-Za-z0-9_-]+$/.test(url.searchParams.get('id') || '')) return '';
const clean = new URL('https://drive.google.com/thumbnail');
clean.searchParams.set('id', url.searchParams.get('id'));
clean.searchParams.set('sz', size);
return clean.href;
} catch (_) { return ''; }
}
const getThumb = d => imageUrl(d, 'w400');
const getFull = d => imageUrl(d, 'w1920');
function populateSelect ($sel, items) {
$sel.empty();
items.forEach(v => $('<option>', { value:v, text:v }).appendTo($sel));
$sel.selectpicker('refresh');
}
function getColumnValues (key) {
return uniqCompact(
flatMap(listData, d => splitCSV(d[key]))
);
}
})();
</script>
動作確認とデプロイ
5ファイルを保存したらスプレッドシートを開き直し、「カスタムメニュー」が表示されるか確認します。
公式のカスタムメニュー資料では、ファイルを開く際のonOpenでメニューを追加します。
「画像パスの取得」を実行し、必要な権限を確認して承認してください。
取得用シートのファイル名と画像URLを管理シートへ転記し、item_nameも入力します。
その後「新しいデプロイ」でWebアプリを選び、実行者を自分、アクセスできるユーザーを自分のみにします。
本人限定にできない環境では公開を進めないでください。
発行されたURLを「ギャラリー」シートのA1に入れ、メニューまたはURLから開きます。
コード変更後はデプロイのバージョンも更新してください。
まず2枚の画像と名称がそろい、画像を押すと拡大表示されるか確認します。
タグ1つで検索し、一致する画像だけになることと、条件を外すと2枚へ戻ることを確かめてください。
ページ分割は19件以上の練習データで、前後のページへ移動できるか確認します。

本例の drive.google.com/thumbnail は恒久的な画像配信用APIとしての動作を保証できません。
画像が表示されない場合はログイン状態・閲覧権限・ブラウザの制限を確認し、表示のためだけに画像を全公開しないでください。
Drive APIのthumbnailLinkも短命で認証条件があり、そのままのWebアプリ埋め込みは推奨されていません。
Driveの公式仕様とGASの実行権限を確認してください。
2026年10月25日に公式資料とコードを照合し、絞り込み関数をローカルで検証しました。
GAS実行・認証画像の表示・CDNの相互動作・画面操作は今回も未検証です。
画像が0件の場合は、管理シートの7列の見出し、画像URL、アイテム名、検索条件を確認してください。
取得用A〜C列の更新は一括で元に戻る処理ではなく、途中失敗で部分的に書き換わる場合があります。
失敗時は追加実行を止め、保存したコピーと比較して取得用シートを復旧してください。
メニューから開けない場合は、A1のURLをブラウザーのアドレス欄へ直接貼って切り分けます。
コードとギャラリーのプレビューを並べて作業する
GASのエディタとWebアプリの動作確認画面を同時に表示しづらい場合は、ブラウザの分割表示やサブモニターの導入が便利です。
運営メディア「デザノマ」のコードとプレビューを並べるモニターの選び方に関する記事も参考にしてみてください。
関連コードは、GitHubのMOTOKI-LLC/cg-method-codeにもあります。
更新時期が異なるため、この手順では本文の5ファイルを一式で使用してください。
GASでウェブアプリケーションを作る記事は、ほかにもあります。




