サイドパネル(Drawer)— タスク詳細パネル
このコンポーネントについて
サイドパネル(ドロワー)は、画面の端からスライドインして表示されるパネル型UIです。ページ遷移なしに詳細情報を確認できるため、一覧と詳細を行き来する業務系アプリケーションで多用されます。
このページでは「タスク一覧の行をクリックすると右からパネルが開く」というパターンを例に、サイドパネルの基本的な実装を紹介します。モーダルと異なり背景(オーバーレイ)クリックで閉じるため、一覧を見ながら詳細を確認するような操作感に向いています。
- タスク一覧テーブル — JSONデータから5件のタスクを動的生成。ステータスと優先度を色付きバッジで表示
- 右スライドパネル — 行の「詳細」ボタンをクリックするとパネルが右端からスライドイン。CSS transitionでなめらかに開閉する
- 背景クリックで閉じる — オーバーレイ(半透明背景)をクリックするとパネルが閉じる
- ESCキー対応 — キーボードでもパネルを閉じられる
- スクロールロック — パネル表示中はページのスクロールをロック
実装のポイント・注意点
パネルの表示・非表示は hidden 属性ではなく、is-open クラスの付け外しで制御しています。hidden を使うと要素が即座に消えるため、CSS の transition が発火する前に非表示になってしまいスライドアニメーションが動きません。オーバーレイには visibility: hidden と opacity: 0 を初期状態として設定し、is-open で両方を戻すことでフェードとスライドを同時に実現しています。
オーバーレイをクリックしたときにパネルを閉じる処理では、e.target === overlay のチェックが重要です。パネル本体(.drw-panel)をクリックしたとき、クリックイベントがオーバーレイまで伝播しますが、e.target(実際にクリックされた要素)がオーバーレイ自身でなければ処理をスキップすることで、パネル内クリックによる誤閉じを防いでいます。
パネルのアニメーションは transform: translateX(100%)(画面右外に待機)→ translateX(0)(表示位置)の切り替えです。right: -380px のような座標指定より transform の方がGPUを使って描画されるため、アニメーションがなめらかです。
スマートフォン対応として max-width: 90vw を設定しています。これにより、小さい画面でもパネルが画面幅を超えて表示されることなく、左端に余白が残ります。テーブルは横スクロール(overflow-x: auto)で対応しています。
テキストの挿入はすべて textContent を使っています。innerHTML に変数を直接渡すとXSS(クロスサイトスクリプティング)の脆弱性になるため、動的なテキスト表示には textContent、動的なDOM生成には createElement + appendChild を使っています。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
| No. | タスク名 | 担当者 | ステータス | 優先度 | 期日 |
|---|
サンプルソース
4つのファイルを同じフォルダに保存してください。ローカルサーバー(VS Code の Live Server 拡張など)経由で index.html を開くと動作確認できます。
ファイル名:index.html / style.css / script.js / tasks.json
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
※ fetch() を使っているため file:// で直接開くと動作しません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>タスク一覧 サイドパネル サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- タスク一覧テーブル(JSで動的生成) -->
<div class="drw-wrap">
<table class="drw-table">
<thead>
<tr>
<th>No.</th>
<th>タスク名</th>
<th>担当者</th>
<th>ステータス</th>
<th>優先度</th>
<th>期日</th>
<th></th>
</tr>
</thead>
<tbody id="drw-tbody"></tbody>
</table>
</div>
<!-- サイドパネル(初期状態は非表示) -->
<div class="drw-overlay" id="drw-overlay">
<div class="drw-panel" id="drw-panel" role="dialog" aria-modal="true" aria-labelledby="drw-panel-title">
<div class="drw-panel-header">
<h2 class="drw-panel-title" id="drw-panel-title"></h2>
<button class="drw-close-btn" id="drw-close-btn" type="button" aria-label="閉じる">✕</button>
</div>
<div class="drw-panel-body">
<div class="drw-badge-row">
<span class="drw-status-badge" id="drw-status"></span>
<span class="drw-priority-badge" id="drw-priority"></span>
</div>
<dl class="drw-meta">
<dt>担当者</dt><dd id="drw-assignee"></dd>
<dt>部署</dt><dd id="drw-department"></dd>
<dt>期日</dt><dd id="drw-due"></dd>
</dl>
<p class="drw-section-label">説明</p>
<p class="drw-desc" id="drw-desc"></p>
<p class="drw-section-label">タグ</p>
<div class="drw-tags" id="drw-tags"></div>
</div>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
:root {
--drw-accent: #2B7FE8;
--drw-panel-width: 380px;
--drw-duration: 0.25s;
}
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
max-width: 900px;
margin: 0 auto;
color: #1A2332;
}
/* パネル表示中はページのスクロールをロック */
body.drw-scroll-lock { overflow: hidden; }
/* ===== テーブル ===== */
.drw-wrap { overflow-x: auto; }
.drw-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.drw-table th,
.drw-table td {
padding: 12px 14px;
text-align: left;
border-bottom: 1px solid #E8EDF2;
white-space: nowrap;
}
.drw-table th {
background: #F4F6F9;
font-size: 12px;
font-weight: 600;
color: #5A6A7A;
letter-spacing: 0.04em;
}
.drw-table tbody tr:hover { background: #F8FAFC; }
/* ===== バッジ共通 ===== */
.drw-status-badge,
.drw-priority-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 100px;
font-size: 12px;
font-weight: 600;
}
/* ステータスバッジ */
.drw-status-badge--in-progress { background: #DBEAFE; color: #1D4ED8; }
.drw-status-badge--todo { background: #F1F5F9; color: #475569; }
.drw-status-badge--done { background: #DCFCE7; color: #166534; }
/* 優先度バッジ */
.drw-priority-badge--high { background: #FEE2E2; color: #991B1B; }
.drw-priority-badge--mid { background: #FEF3C7; color: #92400E; }
.drw-priority-badge--low { background: #DCFCE7; color: #166534; }
/* ===== 詳細ボタン ===== */
.drw-detail-btn {
padding: 5px 14px;
font-size: 12px;
color: var(--drw-accent);
background: #EFF6FF;
border: 1px solid #BFDBFE;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s;
}
.drw-detail-btn:hover { background: #DBEAFE; }
/* ===== オーバーレイ(背景暗幕) ===== */
/* display: none だと transition が効かないため
visibility + opacity で表示を制御する */
.drw-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1000;
visibility: hidden;
opacity: 0;
transition: opacity var(--drw-duration) ease, visibility var(--drw-duration) ease;
}
.drw-overlay.is-open {
visibility: visible;
opacity: 1;
}
/* ===== サイドパネル ===== */
.drw-panel {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: var(--drw-panel-width);
max-width: 90vw; /* スマホで画面幅を超えないようにする */
background: #fff;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
transform: translateX(100%); /* 初期状態:画面右外に待機 */
transition: transform var(--drw-duration) ease;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.drw-overlay.is-open .drw-panel {
transform: translateX(0); /* 表示時:スライドイン */
}
/* ===== パネルヘッダー ===== */
.drw-panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 20px 20px 16px;
border-bottom: 1px solid #E8EDF2;
position: sticky;
top: 0;
background: #fff;
z-index: 1;
}
.drw-panel-title {
font-size: 16px;
font-weight: 700;
margin: 0;
line-height: 1.4;
}
.drw-close-btn {
flex-shrink: 0;
width: 32px;
height: 32px;
background: #F1F5F9;
border: none;
border-radius: 50%;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #5A6A7A;
transition: background 0.15s;
}
.drw-close-btn:hover { background: #E2E8F0; }
/* ===== パネル本文 ===== */
.drw-panel-body {
padding: 20px;
flex: 1;
}
.drw-badge-row {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
/* ===== メタ情報(dl グリッド) ===== */
.drw-meta {
display: grid;
grid-template-columns: auto 1fr;
font-size: 14px;
border: 1px solid #E8EDF2;
border-radius: 8px;
overflow: hidden;
margin: 0 0 20px;
}
.drw-meta dt {
padding: 10px 14px;
background: #F8FAFC;
color: #5A6A7A;
font-weight: 600;
border-bottom: 1px solid #E8EDF2;
}
.drw-meta dd {
padding: 10px 14px;
margin: 0;
color: #1A2332;
border-bottom: 1px solid #E8EDF2;
}
.drw-meta dt:last-of-type,
.drw-meta dd:last-of-type { border-bottom: none; }
/* ===== セクションラベル ===== */
.drw-section-label {
font-size: 11px;
font-weight: 700;
color: #5A6A7A;
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 0 0 8px;
}
/* ===== 説明文 ===== */
.drw-desc {
font-size: 14px;
color: #3A4A5A;
line-height: 1.7;
margin: 0 0 20px;
}
/* ===== タグ ===== */
.drw-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.drw-tag {
padding: 4px 10px;
background: #F1F5F9;
color: #475569;
border-radius: 100px;
font-size: 12px;
font-weight: 500;
}
// =========================================
// タスクデータを JSON から読み込む
// tasks.json を同じフォルダに置いてください
// =========================================
fetch('./tasks.json')
.then(function(res) { return res.json(); })
.then(function(tasks) { renderTable(tasks); })
.catch(function() {
document.getElementById('drw-tbody').innerHTML =
'<tr><td colspan="7" style="text-align:center;color:#888;">データの読み込みに失敗しました。</td></tr>';
});
// =========================================
// テーブル行を生成する
// =========================================
function renderTable(tasks) {
var tbody = document.getElementById('drw-tbody');
tasks.forEach(function(task) {
tbody.appendChild(createRow(task));
});
}
function createRow(task) {
var tr = document.createElement('tr');
var tdNo = document.createElement('td');
tdNo.textContent = task.id;
var tdTitle = document.createElement('td');
tdTitle.textContent = task.title;
var tdAssignee = document.createElement('td');
tdAssignee.textContent = task.assignee;
var tdStatus = document.createElement('td');
var statusBadge = document.createElement('span');
statusBadge.className = 'drw-status-badge drw-status-badge--' + task.statusLevel;
statusBadge.textContent = task.status;
tdStatus.appendChild(statusBadge);
var tdPriority = document.createElement('td');
var priorityBadge = document.createElement('span');
priorityBadge.className = 'drw-priority-badge drw-priority-badge--' + task.priorityLevel;
priorityBadge.textContent = task.priority;
tdPriority.appendChild(priorityBadge);
var tdDue = document.createElement('td');
tdDue.textContent = task.dueDate;
var tdBtn = document.createElement('td');
var btn = document.createElement('button');
btn.className = 'drw-detail-btn';
btn.type = 'button';
btn.textContent = '詳細';
btn.setAttribute('aria-label', task.title + ' の詳細を表示');
// クロージャーで各ボタンに対応するタスクデータを保持する
btn.onclick = function() { openPanel(task); };
tdBtn.appendChild(btn);
tr.appendChild(tdNo);
tr.appendChild(tdTitle);
tr.appendChild(tdAssignee);
tr.appendChild(tdStatus);
tr.appendChild(tdPriority);
tr.appendChild(tdDue);
tr.appendChild(tdBtn);
return tr;
}
// =========================================
// サイドパネルを開く
// =========================================
function openPanel(task) {
var overlay = document.getElementById('drw-overlay');
// タスク名
document.getElementById('drw-panel-title').textContent = task.title;
// ステータスバッジ
var statusEl = document.getElementById('drw-status');
statusEl.textContent = task.status;
statusEl.className = 'drw-status-badge drw-status-badge--' + task.statusLevel;
// 優先度バッジ
var priorityEl = document.getElementById('drw-priority');
priorityEl.textContent = task.priority;
priorityEl.className = 'drw-priority-badge drw-priority-badge--' + task.priorityLevel;
// メタ情報(XSS対策:textContent を使う)
document.getElementById('drw-assignee').textContent = task.assignee;
document.getElementById('drw-department').textContent = task.department;
document.getElementById('drw-due').textContent = task.dueDate;
// 説明文
document.getElementById('drw-desc').textContent = task.description;
// タグ(createElement で1件ずつ生成)
var tagsEl = document.getElementById('drw-tags');
tagsEl.innerHTML = '';
task.tags.forEach(function(tag) {
var span = document.createElement('span');
span.className = 'drw-tag';
span.textContent = tag;
tagsEl.appendChild(span);
});
// is-open クラスを付与してパネルをスライドイン
overlay.classList.add('is-open');
document.body.classList.add('drw-scroll-lock');
// 閉じるボタンにフォーカスを移す(アクセシビリティ対応)
document.getElementById('drw-close-btn').focus();
}
// =========================================
// サイドパネルを閉じる
// =========================================
function closePanel() {
document.getElementById('drw-overlay').classList.remove('is-open');
document.body.classList.remove('drw-scroll-lock');
}
// オーバーレイクリックで閉じる(パネル本体クリックは除外)
document.getElementById('drw-overlay').addEventListener('click', function(e) {
if (e.target === this) closePanel();
});
// × ボタンで閉じる
document.getElementById('drw-close-btn').addEventListener('click', closePanel);
// ESCキーで閉じる
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closePanel();
});
[
{
"id": 1,
"title": "要件定義書の作成",
"assignee": "田中太郎",
"department": "企画部",
"status": "進行中",
"statusLevel": "in-progress",
"priority": "高",
"priorityLevel": "high",
"dueDate": "2026-06-10",
"description": "新規プロジェクトの要件定義書を作成する。ステークホルダーへのヒアリングを完了し、機能一覧・非機能要件をまとめること。",
"tags": ["ドキュメント", "企画"]
},
{
"id": 2,
"title": "UIデザインレビュー",
"assignee": "鈴木花子",
"department": "デザイン部",
"status": "未着手",
"statusLevel": "todo",
"priority": "中",
"priorityLevel": "mid",
"dueDate": "2026-06-15",
"description": "新機能のUIデザイン案をレビューし、フィードバックを提供する。アクセシビリティ・スマホ対応の観点も確認すること。",
"tags": ["デザイン", "レビュー"]
},
{
"id": 3,
"title": "APIエンドポイントの実装",
"assignee": "佐藤次郎",
"department": "開発部",
"status": "進行中",
"statusLevel": "in-progress",
"priority": "高",
"priorityLevel": "high",
"dueDate": "2026-06-08",
"description": "ユーザー管理・認証に関するAPIエンドポイントを実装する。OpenAPI仕様書に沿って実装し、単体テストも作成すること。",
"tags": ["バックエンド", "API"]
},
{
"id": 4,
"title": "テスト仕様書の整備",
"assignee": "山田三郎",
"department": "QA部",
"status": "完了",
"statusLevel": "done",
"priority": "低",
"priorityLevel": "low",
"dueDate": "2026-05-30",
"description": "リリース前の結合テスト仕様書を整備する。テストケースの網羅性を確認し、担当者へ展開すること。",
"tags": ["テスト", "ドキュメント"]
},
{
"id": 5,
"title": "本番環境デプロイ",
"assignee": "中村四郎",
"department": "インフラ部",
"status": "未着手",
"statusLevel": "todo",
"priority": "高",
"priorityLevel": "high",
"dueDate": "2026-06-20",
"description": "本番環境へのデプロイ作業を実施する。リリース手順書に従い、ロールバック手順も確認した上で作業を進めること。",
"tags": ["インフラ", "デプロイ"]
}
]
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。データ件数の変更や表示項目の追加など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# サイドパネル(ドロワー)作成依頼
## 概要
タスク一覧テーブルの行をクリックすると、右からサイドパネルがスライドインして詳細を表示するUIを実装してください。
タスクデータはJavaScript配列で定義します。
## 要件
- タスクデータをJavaScript配列で定義する(id・title・assignee・department・status・statusLevel・priority・priorityLevel・dueDate・description・tagsを含む)
- タスクをテーブル(No.・タスク名・担当者・ステータス・優先度・期日・詳細ボタン)で表示する
- 「詳細」ボタンをクリックすると右からサイドパネルがスライドインする
- パネル内に:タスク名・ステータスバッジ・優先度バッジ・担当者・部署・期日・説明文・タグを表示する
- 半透明オーバーレイ(背景)を表示し、クリックでパネルを閉じる
- パネル右上の × ボタンでも閉じる
- ESCキーでも閉じる
- パネル表示中はページのスクロールをロックする(body に overflow: hidden)
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(テーブルは横スクロール、パネルは max-width: 90vw)
## 動作詳細
テーブルの「詳細」ボタンをクリックするとサイドパネルが開く。
パネルはページ右端に固定(position: fixed; top: 0; right: 0; height: 100%)し、初期状態は translateX(100%) で画面外に待機。
.is-open クラスの付与・除去でパネルを開閉し、CSS transitionでスライドアニメーションを実現する。
オーバーレイは visibility + opacity で制御する(display: none だと transition が効かないため)。
背景のオーバーレイをクリックしたときもパネルを閉じる(e.target === overlay のチェックで誤閉じを防ぐ)。
ステータスは「進行中」「未着手」「完了」の3種、優先度は「高」「中」「低」の3種とし、それぞれ色違いのバッジで表示する。
テキストの出力はすべて textContent を使いXSS対策を徹底する。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。