Googleドライブ内の画像を一覧表示するギャラリーを作成しました。
本構成のメリットは以下の通りです。
- Googleの標準サービスを利用するため追加コストがかからない
- アクセス権を持つGoogleユーザーのみ閲覧可能でセキュア
- GASで開発するためカスタマイズ性が高く&動的にギャラリーを生成できる
- スプレッドシート上でデータを編集・管理しやすい
- Googleドライブ上の画像データを直接利用できる&ストレージ容量も確保しやすい

ギャラリー表示機能の概要
作成したギャラリーの基本機能を紹介します。
各要素の連携と画面上の動作イメージを確認できます。
1. Googleドライブの設定
専用フォルダを作成し、ギャラリーに表示したい画像ファイルを格納しておきます。

2. スプレッドシートへのデータ入力
スプレッドシートをデータベースとして利用します。
IDや名称に加え、絞り込みに使用するタグや種別などの情報を入力します。

※画像のパス取得には、別途用意した専用機能を利用します。

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

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

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

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

| main.gs | バックエンドの処理実装 |
|---|---|
| index.html | 画面の基本HTML構造 |
| css.html | レイアウトやホバー演出のスタイル |
| js.html | 表示制御や絞り込み検索のスクリプト |
| plugin.html | 外部ライブラリの読み込み設定 |
以下は実装の詳細なコードと解説です。
※本コードはベースサンプルです。
要件に応じてカスタマイズしてご活用ください。
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
- 画像パスを出力するシート名
- メタデータを管理しているシート名
各項目をお手元の環境に合わせて設定してください。
シート上から各機能を呼び出せるよう、カスタムメニューも追加しています。

※公開したWebアプリのURLは、管理用シートの所定セルに転記して管理します。

/* =====================================================================
* 定数定義
* ===================================================================*/
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);
if (!list.length) return;
const sheet = getSheet(SHEET_IMG_PATH);
sheet.clearContents();
sheet.getRange(1, 1, 1, 3).setValues([['ファイル名', '画像URL', 'フォルダパス']]);
sheet.getRange(2, 1, list.length, 3).setValues(list);
}
function getFilesRecursively(folder, path, list) {
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
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);
}
}
/* =====================================================================
* Web アプリ公開
* ===================================================================*/
function doGet () {
return HtmlService
.createTemplateFromFile('index')
.evaluate();
}
/* 新しいタブで Web アプリを開く(メニュー用)*/
function openViewer () {
const url = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('ギャラリー')
.getRange('A1')
.getValue();
const safeUrl = JSON.stringify(url);
const script = `
<script>
window.open(${safeUrl}, '_blank').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);
return ss.getSheetByName(name);
}
/** ヘッダー行をキーに行データをオブジェクト配列化 */
function readSheetObjects (sheetName) {
const sheet = getSheet(sheetName);
const [header, ...rows] = sheet.getDataRange().getDisplayValues();
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>
<div id="grid"></div>
</ul>
</main>
<!-- ================= 画像モーダル ================= -->
<div class="iziModal"></div>
<!-- ================= JavaScript ================= -->
<?!= include('js') ?>
</body>
</html>
js.htmlの実装
フロントエンド側の表示制御を行うJavaScriptコードです。
ホバー時に表示するデータ項目など、描画ロジックは必要に応じて変更してください。
<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 = []; // スプレッドシート全件
let listData = []; // 画像あり・有効フラグ付きデータ
let filterKeys = ["rare", "type"];
let chunks = []; // ページング用
let selFilter = [], selChara = [], selTags = [];
/* ------------------------------------------------------------------
* 3. 初期化
* ------------------------------------------------------------------*/
$(async () => {
$('#btn-search').on('click', onSearchClick);
initModal();
await loadSheetData();
buildUI();
});
/* ------------------------------------------------------------------
* 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">`;
$modal.iziModal('setContent', html);
$modal.iziModal('open');
};
img.src = full;
});
}
/* ------------------------------------------------------------------
* 5. データ取得
* ------------------------------------------------------------------*/
async function loadSheetData () {
const json = await new Promise(resolve => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(() => alert('データ取得失敗'))
.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 = _.intersection(selected, keyOpts);
if (!hit.length) return true;
const rowVals = splitCSV(row[key]);
return !rowVals.length || 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();
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. フィルターセレクト生成
* ------------------------------------------------------------------*/
async function getFilterHeader () {
const json = await new Promise(resolve => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(() => alert('フィルターキー取得失敗'))
.getFilterKeys();
});
return JSON.parse(json);
}
function refreshFilterSelect () {
$filter.empty();
filterKeys.forEach(key => {
const opts = getColumnValues(key);
const $group = $('<optgroup>', { label: key });
opts.forEach(v => $('<option>', { value: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 => (str ? str.split(',') : []);
const getThumb = d => d.image_path ? `${d.image_path}&sz=w400` : '';
const getFull = d => d.image_path ? `${d.image_path}&sz=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>
動作確認とデプロイ
すべてのコードを保存後、「新しいデプロイ」からウェブアプリとして公開し、URLにアクセスして表示を確認します。
画像一覧の表示だけでなく、モーダル表示、ホバー時の情報表示、絞り込み検索が正常に動作するか確認してください。

以上で、Googleスプレッドシートとドライブを連携した画像ギャラリーの構築は完了です。
コードとギャラリーのプレビューを並べて作業する
GASのエディタとWebアプリの動作確認画面を同時に表示しづらい場合は、ブラウザの分割表示やサブモニターの導入が便利です。
運営メディア「デザノマ」のコードとプレビューを並べるモニターの選び方に関する記事も参考にしてみてください。
この記事で紹介しているコードは、GitHubのMOTOKI-LLC/cg-method-codeにもまとめています。
GASでウェブアプリケーションを作る記事は、ほかにもあります。




