コマンドパレット(⌘K / Ctrl+K)
⌘K/Ctrl+Kで開く検索ボックス付きモーダル。入力でフィルタ、↑↓で候補移動、Enterで実行、Escで閉じます。アプリの素早い操作導線に使えます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:ダッシュボードを素早く操作するコマンドパレット(⌘K / Ctrl+K) -->
<div class="app">
<header class="app__bar">
<span class="app__logo">💼 FlowDesk</span>
<button class="kbtn" data-open><kbd><span data-mod>Ctrl</span></kbd>+<kbd>K</kbd> で操作</button>
</header>
<main class="dash">
<div class="kpi"><span class="kpi__label">進行中タスク</span><strong class="kpi__num">128</strong></div>
<div class="kpi"><span class="kpi__label">今週の完了</span><strong class="kpi__num">42</strong></div>
<div class="kpi"><span class="kpi__label">メンバー</span><strong class="kpi__num">9</strong></div>
<p class="dash__hint" data-result>⌘K / Ctrl+K でコマンドを実行できます</p>
</main>
<!-- コマンドパレット本体 -->
<div class="cp" data-overlay hidden>
<div class="cp__panel" role="dialog" aria-label="コマンドパレット">
<div class="cp__search">
<span class="cp__mag">🔍</span>
<input class="cp__input" data-input type="text" placeholder="コマンドやページを検索…" aria-label="コマンド検索">
</div>
<ul class="cp__list" data-list role="listbox"></ul>
<p class="cp__empty" data-empty hidden>該当するコマンドがありません</p>
<div class="cp__foot"><span><kbd>↑</kbd><kbd>↓</kbd> 移動</span><span><kbd>Enter</kbd> 実行</span><span><kbd>Esc</kbd> 閉じる</span></div>
</div>
</div>
<div class="cp__toast" data-toast hidden></div>
</div>
CSS
/* FlowDesk SaaS テーマ */
:root{--navy:#0f1b34;--blue:#4f7cff;--ink:#1d2740;--line:#e3e8f2;--muted:#6b7794;--bg:#f4f6fb}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--ink)}
.app{position:relative;max-width:520px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column}
.app__bar{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--navy);color:#fff}
.app__logo{font-weight:700}
.kbtn{appearance:none;cursor:pointer;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#dbe3f7;font:inherit;font-size:.78rem;padding:6px 10px;border-radius:8px}
.kbtn kbd{background:rgba(255,255,255,.15);border-radius:4px;padding:1px 5px;font-family:inherit}
.dash{flex:1;padding:22px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;align-content:start}
.kpi{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px}
.kpi__label{display:block;font-size:.74rem;color:var(--muted)}
.kpi__num{display:block;margin-top:6px;font-size:1.7rem;font-weight:800;color:var(--navy)}
.dash__hint{grid-column:1/-1;margin:6px 0 0;font-size:.82rem;color:var(--muted);transition:color .2s}
.dash__hint.is-hit{color:var(--blue);font-weight:700}
/* コマンドパレット */
.cp{position:absolute;inset:0;background:rgba(15,27,52,.45);backdrop-filter:blur(2px);display:flex;justify-content:center;align-items:flex-start;padding-top:56px;z-index:20}
.cp__panel{width:min(420px,92%);background:#fff;border-radius:16px;box-shadow:0 24px 60px rgba(15,27,52,.4);overflow:hidden;animation:drop .2s ease}
@keyframes drop{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:none}}
.cp__search{display:flex;align-items:center;gap:10px;padding:13px 16px;border-bottom:1px solid var(--line)}
.cp__mag{font-size:.95rem}
.cp__input{flex:1;border:none;outline:none;font:inherit;font-size:.95rem;background:none;color:var(--ink)}
.cp__list{list-style:none;margin:0;padding:6px;max-height:200px;overflow:auto}
.cp-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:9px;cursor:pointer}
.cp-item.is-active{background:#eef3ff}
.cp-item__icon{font-size:1rem}
.cp-item__name{flex:1;font-size:.9rem}
.cp-item__hint{font-size:.72rem;color:var(--muted);background:var(--bg);border:1px solid var(--line);border-radius:5px;padding:1px 7px}
.cp__empty{margin:0;padding:18px;text-align:center;font-size:.85rem;color:var(--muted)}
.cp__foot{display:flex;gap:16px;padding:10px 16px;border-top:1px solid var(--line);font-size:.72rem;color:var(--muted)}
.cp__foot kbd{background:var(--bg);border:1px solid var(--line);border-radius:4px;padding:1px 5px;margin-right:3px}
.cp__toast{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);background:var(--navy);color:#fff;padding:10px 18px;border-radius:10px;font-size:.84rem;box-shadow:0 10px 30px rgba(15,27,52,.3);z-index:21}
@media (prefers-reduced-motion:reduce){.cp__panel{animation:none}.dash__hint{transition:none}}
JavaScript
// コマンドパレット:⌘K/Ctrl+K で開閉、↑↓で移動、Enterで実行、Escで閉じる
const overlay = document.querySelector('[data-overlay]');
const input = document.querySelector('[data-input]');
const listEl = document.querySelector('[data-list]');
const emptyEl = document.querySelector('[data-empty]');
const openBtn = document.querySelector('[data-open]');
const resultEl = document.querySelector('[data-result]');
const toastEl = document.querySelector('[data-toast]');
const modKbd = document.querySelector('[data-mod]');
// FlowDesk のダッシュボード操作コマンド
const COMMANDS = [
{ id: 'task', icon: '✅', name: '新しいタスクを作成', hint: 'N' },
{ id: 'invite', icon: '👤', name: 'メンバーを招待', hint: 'I' },
{ id: 'project', icon: '📁', name: 'プロジェクトに移動', hint: 'P' },
{ id: 'report', icon: '📊', name: '週次レポートを表示', hint: 'R' },
{ id: 'billing', icon: '💳', name: 'プランと請求を開く', hint: 'B' },
{ id: 'search', icon: '🔍', name: '全タスクを検索', hint: 'F' },
{ id: 'settings', icon: '⚙️', name: 'ワークスペース設定', hint: ',' },
{ id: 'help', icon: '❓', name: 'ヘルプセンター', hint: '?' },
];
if (overlay && input && listEl) {
// Mac 判定で表示キーを ⌘ に
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform || '');
if (isMac && modKbd) modKbd.textContent = '⌘';
let items = [];
let active = 0;
let toastTimer = null;
// フィルタして候補を描画
const render = () => {
const q = input.value.trim().toLowerCase();
const matched = q
? COMMANDS.filter((c) => c.name.toLowerCase().includes(q))
: COMMANDS.slice();
listEl.innerHTML = '';
matched.forEach((c, i) => {
const li = document.createElement('li');
li.className = 'cp-item' + (i === 0 ? ' is-active' : '');
li.setAttribute('role', 'option');
li.dataset.name = c.name;
li.innerHTML =
'<span class="cp-item__icon">' + c.icon + '</span>' +
'<span class="cp-item__name"></span>' +
'<span class="cp-item__hint">' + c.hint + '</span>';
li.querySelector('.cp-item__name').textContent = c.name; // 安全に挿入
li.addEventListener('mousemove', () => setActive(i));
li.addEventListener('click', () => execAt(i));
listEl.appendChild(li);
});
items = Array.from(listEl.children);
active = 0;
if (emptyEl) emptyEl.hidden = items.length > 0;
};
// 選択ハイライトを更新
const setActive = (i) => {
if (!items.length) return;
active = (i + items.length) % items.length;
items.forEach((el, n) => el.classList.toggle('is-active', n === active));
items[active].scrollIntoView({ block: 'nearest' });
};
// 指定位置の候補を実行
const execAt = (i) => {
const el = items[i];
if (!el) return;
const name = el.dataset.name || '';
close();
if (resultEl) {
resultEl.textContent = '実行: ' + name;
resultEl.classList.add('is-hit');
}
showToast('▶ ' + name);
};
// トーストを一時表示
const showToast = (msg) => {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.hidden = false;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastEl.hidden = true; }, 1800);
};
const open = () => { overlay.hidden = false; input.value = ''; render(); input.focus(); };
const close = () => { overlay.hidden = true; };
if (openBtn) openBtn.addEventListener('click', open);
input.addEventListener('input', render);
// パレット内のキー操作
overlay.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(active + 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(active - 1); }
else if (e.key === 'Enter') { e.preventDefault(); execAt(active); }
else if (e.key === 'Escape') { e.preventDefault(); close(); }
});
// 背景クリックで閉じる
overlay.addEventListener('mousedown', (e) => { if (e.target === overlay) close(); });
// ⌘K / Ctrl+K を捕捉
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
overlay.hidden ? open() : close();
}
});
}
コード
HTML
<!-- コマンドパレット:⌘K / Ctrl+K で開く検索モーダル。↑↓で移動・Enterで実行・Escで閉じる -->
<div class="cp-stage">
<div class="cp-hero">
<p class="cp-hero__lead">どこでも素早くコマンド実行</p>
<button class="cp-trigger" data-open>
検索 / コマンド
<kbd class="cp-kbd"><span data-mod>Ctrl</span> K</kbd>
</button>
<p class="cp-result" data-result aria-live="polite">準備OK</p>
</div>
<!-- パレット本体(iframe領域内に fixed で収める) -->
<div class="cp-overlay" data-overlay hidden>
<div class="cp-panel" role="dialog" aria-label="コマンドパレット" aria-modal="true">
<div class="cp-search">
<span class="cp-search__icon" aria-hidden="true">⌕</span>
<input class="cp-search__input" data-input type="text"
placeholder="コマンドを入力…" autocomplete="off" spellcheck="false">
<kbd class="cp-kbd cp-kbd--esc">Esc</kbd>
</div>
<ul class="cp-list" data-list role="listbox"></ul>
<p class="cp-empty" data-empty hidden>該当する候補がありません</p>
<div class="cp-foot">
<span><kbd class="cp-hint">↑</kbd><kbd class="cp-hint">↓</kbd> 移動</span>
<span><kbd class="cp-hint">Enter</kbd> 実行</span>
<span><kbd class="cp-hint">Esc</kbd> 閉じる</span>
</div>
</div>
</div>
<!-- 実行フィードバック用トースト -->
<div class="cp-toast" data-toast hidden></div>
</div>
CSS
:root{
--bg:#0b1020;
--panel:#161c30;
--panel2:#1e253c;
--text:#e7ebf5;
--muted:#9aa3bd;
--accent:#818cf8;
--accent2:#a5b4fc;
--line:rgba(129,140,248,.18);
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;position:relative;overflow:hidden;
display:grid;place-items:center;padding:24px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:radial-gradient(700px 360px at 50% -10%,#222c5c,transparent),var(--bg);
}
.cp-stage{width:min(440px,100%);text-align:center}
.cp-hero__lead{margin:0 0 16px;color:var(--muted);font-size:.92rem}
/* 起動ボタン */
.cp-trigger{
display:inline-flex;align-items:center;gap:12px;
font:inherit;font-weight:600;cursor:pointer;color:var(--text);
background:var(--panel);border:1px solid var(--line);border-radius:12px;
padding:12px 14px 12px 18px;transition:border-color .2s,transform .15s;
}
.cp-trigger:hover{border-color:var(--accent)}
.cp-trigger:active{transform:translateY(1px)}
.cp-kbd{
font:inherit;font-size:.72rem;font-weight:700;color:var(--muted);
background:var(--panel2);border:1px solid var(--line);border-radius:7px;
padding:3px 8px;letter-spacing:.04em;
}
.cp-kbd--esc{flex:none}
.cp-result{margin:18px 0 0;color:var(--muted);font-size:.85rem;min-height:1.2em}
.cp-result.is-hit{color:var(--accent2);font-weight:600}
/* オーバーレイ(iframe領域内に収める) */
.cp-overlay{
position:fixed;inset:0;z-index:40;
display:flex;justify-content:center;align-items:flex-start;
padding:36px 18px 18px;
background:rgba(6,10,22,.62);backdrop-filter:blur(4px);
animation:cpFade .18s ease;
}
.cp-overlay[hidden]{display:none}
.cp-panel{
width:min(420px,100%);
background:var(--panel);border:1px solid var(--line);border-radius:16px;
box-shadow:0 40px 80px -28px rgba(0,0,0,.8);
overflow:hidden;text-align:left;
animation:cpPop .22s cubic-bezier(.2,.9,.3,1.2);
}
/* 検索行 */
.cp-search{
display:flex;align-items:center;gap:10px;
padding:12px 14px;border-bottom:1px solid var(--line);
}
.cp-search__icon{color:var(--muted);font-size:1.15rem;line-height:1}
.cp-search__input{
flex:1;min-width:0;font:inherit;font-size:.96rem;
background:none;border:none;outline:none;color:var(--text);
}
.cp-search__input::placeholder{color:var(--muted)}
/* 候補リスト */
.cp-list{
list-style:none;margin:0;padding:6px;max-height:188px;overflow:auto;
display:flex;flex-direction:column;gap:2px;
}
.cp-item{
display:flex;align-items:center;gap:12px;
padding:9px 11px;border-radius:9px;cursor:pointer;
font-size:.9rem;color:var(--text);transition:background .12s;
}
.cp-item__icon{
flex:none;width:26px;height:26px;border-radius:7px;
display:grid;place-items:center;font-size:.9rem;
background:var(--panel2);
}
.cp-item__name{flex:1;min-width:0}
.cp-item__hint{font-size:.72rem;color:var(--muted)}
.cp-item.is-active{background:linear-gradient(135deg,rgba(129,140,248,.28),rgba(129,140,248,.16))}
.cp-item.is-active .cp-item__hint{color:var(--accent2)}
.cp-empty{margin:0;padding:22px;text-align:center;color:var(--muted);font-size:.88rem}
/* フッター */
.cp-foot{
display:flex;gap:16px;flex-wrap:wrap;
padding:9px 14px;border-top:1px solid var(--line);
color:var(--muted);font-size:.74rem;
}
.cp-hint{
font:inherit;font-size:.7rem;color:var(--muted);
background:var(--panel2);border:1px solid var(--line);border-radius:5px;
padding:1px 6px;margin-right:4px;
}
/* 実行トースト */
.cp-toast{
position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:50;
background:var(--panel2);border:1px solid var(--accent);border-radius:11px;
padding:11px 18px;font-size:.88rem;font-weight:600;color:var(--text);
box-shadow:0 18px 36px -18px rgba(0,0,0,.8);
animation:cpToast .25s cubic-bezier(.2,.9,.3,1.2);
}
.cp-toast[hidden]{display:none}
@keyframes cpFade{from{opacity:0}to{opacity:1}}
@keyframes cpPop{from{opacity:0;transform:translateY(-10px) scale(.97)}to{opacity:1;transform:none}}
@keyframes cpToast{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%)}}
@media (prefers-reduced-motion:reduce){
.cp-overlay,.cp-panel,.cp-toast{animation:none}
}
JavaScript
// コマンドパレット:⌘K/Ctrl+K で開閉、↑↓で移動、Enterで実行、Escで閉じる
const overlay = document.querySelector('[data-overlay]');
const input = document.querySelector('[data-input]');
const listEl = document.querySelector('[data-list]');
const emptyEl = document.querySelector('[data-empty]');
const openBtn = document.querySelector('[data-open]');
const resultEl = document.querySelector('[data-result]');
const toastEl = document.querySelector('[data-toast]');
const modKbd = document.querySelector('[data-mod]');
// 候補コマンド一覧(アイコン+ショートカット表示用)
const COMMANDS = [
{ id: 'new', icon: '📄', name: '新規ファイルを作成', hint: 'N' },
{ id: 'open', icon: '📂', name: 'ファイルを開く', hint: 'O' },
{ id: 'search', icon: '🔍', name: 'プロジェクト内を検索', hint: 'F' },
{ id: 'theme', icon: '🎨', name: 'テーマを切り替え', hint: 'T' },
{ id: 'settings', icon: '⚙️', name: '設定を開く', hint: ',' },
{ id: 'share', icon: '🔗', name: '共有リンクをコピー', hint: 'S' },
{ id: 'export', icon: '⬇️', name: 'PDFでエクスポート', hint: 'E' },
{ id: 'help', icon: '❓', name: 'ヘルプを表示', hint: '?' },
];
if (overlay && input && listEl) {
// Mac 判定で表示キーを ⌘ に
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform || '');
if (isMac && modKbd) modKbd.textContent = '⌘';
let items = []; // 現在の DOM 要素配列
let active = 0; // 選択中インデックス
let toastTimer = null;
// フィルタして候補を描画
const render = () => {
const q = input.value.trim().toLowerCase();
const matched = q
? COMMANDS.filter((c) => c.name.toLowerCase().includes(q))
: COMMANDS.slice();
listEl.innerHTML = '';
matched.forEach((c, i) => {
const li = document.createElement('li');
li.className = 'cp-item' + (i === 0 ? ' is-active' : '');
li.setAttribute('role', 'option');
li.dataset.name = c.name;
li.innerHTML =
'<span class="cp-item__icon">' + c.icon + '</span>' +
'<span class="cp-item__name"></span>' +
'<span class="cp-item__hint">' + c.hint + '</span>';
// ユーザー入力由来でない固定文字列だが name は textContent で安全に
li.querySelector('.cp-item__name').textContent = c.name;
li.addEventListener('mousemove', () => setActive(i));
li.addEventListener('click', () => execAt(i));
listEl.appendChild(li);
});
items = Array.from(listEl.children);
active = 0;
if (emptyEl) emptyEl.hidden = items.length > 0;
};
// 選択ハイライトを更新
const setActive = (i) => {
if (!items.length) return;
active = (i + items.length) % items.length;
items.forEach((el, n) => el.classList.toggle('is-active', n === active));
items[active].scrollIntoView({ block: 'nearest' });
};
// 指定位置の候補を実行
const execAt = (i) => {
const el = items[i];
if (!el) return;
const name = el.dataset.name || '';
close();
if (resultEl) {
resultEl.textContent = '実行: ' + name;
resultEl.classList.add('is-hit');
}
showToast('▶ ' + name);
};
// トーストを一時表示
const showToast = (msg) => {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.hidden = false;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastEl.hidden = true; }, 1800);
};
const open = () => {
overlay.hidden = false;
input.value = '';
render();
input.focus();
};
const close = () => {
overlay.hidden = true;
};
if (openBtn) openBtn.addEventListener('click', open);
// 入力でフィルタ
input.addEventListener('input', render);
// パレット内のキー操作
overlay.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(active + 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(active - 1); }
else if (e.key === 'Enter') { e.preventDefault(); execAt(active); }
else if (e.key === 'Escape') { e.preventDefault(); close(); }
});
// 背景クリックで閉じる
overlay.addEventListener('mousedown', (e) => {
if (e.target === overlay) close();
});
// ⌘K / Ctrl+K を iframe 内で捕捉(document レベル)
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
overlay.hidden ? open() : close();
}
});
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「コマンドパレット(⌘K / Ctrl+K)」の効果を追加してください。
# 追加してほしい効果
コマンドパレット(⌘K / Ctrl+K)(UIコンポーネント)
⌘K/Ctrl+Kで開く検索ボックス付きモーダル。入力でフィルタ、↑↓で候補移動、Enterで実行、Escで閉じます。アプリの素早い操作導線に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- コマンドパレット:⌘K / Ctrl+K で開く検索モーダル。↑↓で移動・Enterで実行・Escで閉じる -->
<div class="cp-stage">
<div class="cp-hero">
<p class="cp-hero__lead">どこでも素早くコマンド実行</p>
<button class="cp-trigger" data-open>
検索 / コマンド
<kbd class="cp-kbd"><span data-mod>Ctrl</span> K</kbd>
</button>
<p class="cp-result" data-result aria-live="polite">準備OK</p>
</div>
<!-- パレット本体(iframe領域内に fixed で収める) -->
<div class="cp-overlay" data-overlay hidden>
<div class="cp-panel" role="dialog" aria-label="コマンドパレット" aria-modal="true">
<div class="cp-search">
<span class="cp-search__icon" aria-hidden="true">⌕</span>
<input class="cp-search__input" data-input type="text"
placeholder="コマンドを入力…" autocomplete="off" spellcheck="false">
<kbd class="cp-kbd cp-kbd--esc">Esc</kbd>
</div>
<ul class="cp-list" data-list role="listbox"></ul>
<p class="cp-empty" data-empty hidden>該当する候補がありません</p>
<div class="cp-foot">
<span><kbd class="cp-hint">↑</kbd><kbd class="cp-hint">↓</kbd> 移動</span>
<span><kbd class="cp-hint">Enter</kbd> 実行</span>
<span><kbd class="cp-hint">Esc</kbd> 閉じる</span>
</div>
</div>
</div>
<!-- 実行フィードバック用トースト -->
<div class="cp-toast" data-toast hidden></div>
</div>
【CSS】
:root{
--bg:#0b1020;
--panel:#161c30;
--panel2:#1e253c;
--text:#e7ebf5;
--muted:#9aa3bd;
--accent:#818cf8;
--accent2:#a5b4fc;
--line:rgba(129,140,248,.18);
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;position:relative;overflow:hidden;
display:grid;place-items:center;padding:24px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:radial-gradient(700px 360px at 50% -10%,#222c5c,transparent),var(--bg);
}
.cp-stage{width:min(440px,100%);text-align:center}
.cp-hero__lead{margin:0 0 16px;color:var(--muted);font-size:.92rem}
/* 起動ボタン */
.cp-trigger{
display:inline-flex;align-items:center;gap:12px;
font:inherit;font-weight:600;cursor:pointer;color:var(--text);
background:var(--panel);border:1px solid var(--line);border-radius:12px;
padding:12px 14px 12px 18px;transition:border-color .2s,transform .15s;
}
.cp-trigger:hover{border-color:var(--accent)}
.cp-trigger:active{transform:translateY(1px)}
.cp-kbd{
font:inherit;font-size:.72rem;font-weight:700;color:var(--muted);
background:var(--panel2);border:1px solid var(--line);border-radius:7px;
padding:3px 8px;letter-spacing:.04em;
}
.cp-kbd--esc{flex:none}
.cp-result{margin:18px 0 0;color:var(--muted);font-size:.85rem;min-height:1.2em}
.cp-result.is-hit{color:var(--accent2);font-weight:600}
/* オーバーレイ(iframe領域内に収める) */
.cp-overlay{
position:fixed;inset:0;z-index:40;
display:flex;justify-content:center;align-items:flex-start;
padding:36px 18px 18px;
background:rgba(6,10,22,.62);backdrop-filter:blur(4px);
animation:cpFade .18s ease;
}
.cp-overlay[hidden]{display:none}
.cp-panel{
width:min(420px,100%);
background:var(--panel);border:1px solid var(--line);border-radius:16px;
box-shadow:0 40px 80px -28px rgba(0,0,0,.8);
overflow:hidden;text-align:left;
animation:cpPop .22s cubic-bezier(.2,.9,.3,1.2);
}
/* 検索行 */
.cp-search{
display:flex;align-items:center;gap:10px;
padding:12px 14px;border-bottom:1px solid var(--line);
}
.cp-search__icon{color:var(--muted);font-size:1.15rem;line-height:1}
.cp-search__input{
flex:1;min-width:0;font:inherit;font-size:.96rem;
background:none;border:none;outline:none;color:var(--text);
}
.cp-search__input::placeholder{color:var(--muted)}
/* 候補リスト */
.cp-list{
list-style:none;margin:0;padding:6px;max-height:188px;overflow:auto;
display:flex;flex-direction:column;gap:2px;
}
.cp-item{
display:flex;align-items:center;gap:12px;
padding:9px 11px;border-radius:9px;cursor:pointer;
font-size:.9rem;color:var(--text);transition:background .12s;
}
.cp-item__icon{
flex:none;width:26px;height:26px;border-radius:7px;
display:grid;place-items:center;font-size:.9rem;
background:var(--panel2);
}
.cp-item__name{flex:1;min-width:0}
.cp-item__hint{font-size:.72rem;color:var(--muted)}
.cp-item.is-active{background:linear-gradient(135deg,rgba(129,140,248,.28),rgba(129,140,248,.16))}
.cp-item.is-active .cp-item__hint{color:var(--accent2)}
.cp-empty{margin:0;padding:22px;text-align:center;color:var(--muted);font-size:.88rem}
/* フッター */
.cp-foot{
display:flex;gap:16px;flex-wrap:wrap;
padding:9px 14px;border-top:1px solid var(--line);
color:var(--muted);font-size:.74rem;
}
.cp-hint{
font:inherit;font-size:.7rem;color:var(--muted);
background:var(--panel2);border:1px solid var(--line);border-radius:5px;
padding:1px 6px;margin-right:4px;
}
/* 実行トースト */
.cp-toast{
position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:50;
background:var(--panel2);border:1px solid var(--accent);border-radius:11px;
padding:11px 18px;font-size:.88rem;font-weight:600;color:var(--text);
box-shadow:0 18px 36px -18px rgba(0,0,0,.8);
animation:cpToast .25s cubic-bezier(.2,.9,.3,1.2);
}
.cp-toast[hidden]{display:none}
@keyframes cpFade{from{opacity:0}to{opacity:1}}
@keyframes cpPop{from{opacity:0;transform:translateY(-10px) scale(.97)}to{opacity:1;transform:none}}
@keyframes cpToast{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%)}}
@media (prefers-reduced-motion:reduce){
.cp-overlay,.cp-panel,.cp-toast{animation:none}
}
【JavaScript】
// コマンドパレット:⌘K/Ctrl+K で開閉、↑↓で移動、Enterで実行、Escで閉じる
const overlay = document.querySelector('[data-overlay]');
const input = document.querySelector('[data-input]');
const listEl = document.querySelector('[data-list]');
const emptyEl = document.querySelector('[data-empty]');
const openBtn = document.querySelector('[data-open]');
const resultEl = document.querySelector('[data-result]');
const toastEl = document.querySelector('[data-toast]');
const modKbd = document.querySelector('[data-mod]');
// 候補コマンド一覧(アイコン+ショートカット表示用)
const COMMANDS = [
{ id: 'new', icon: '📄', name: '新規ファイルを作成', hint: 'N' },
{ id: 'open', icon: '📂', name: 'ファイルを開く', hint: 'O' },
{ id: 'search', icon: '🔍', name: 'プロジェクト内を検索', hint: 'F' },
{ id: 'theme', icon: '🎨', name: 'テーマを切り替え', hint: 'T' },
{ id: 'settings', icon: '⚙️', name: '設定を開く', hint: ',' },
{ id: 'share', icon: '🔗', name: '共有リンクをコピー', hint: 'S' },
{ id: 'export', icon: '⬇️', name: 'PDFでエクスポート', hint: 'E' },
{ id: 'help', icon: '❓', name: 'ヘルプを表示', hint: '?' },
];
if (overlay && input && listEl) {
// Mac 判定で表示キーを ⌘ に
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform || '');
if (isMac && modKbd) modKbd.textContent = '⌘';
let items = []; // 現在の DOM 要素配列
let active = 0; // 選択中インデックス
let toastTimer = null;
// フィルタして候補を描画
const render = () => {
const q = input.value.trim().toLowerCase();
const matched = q
? COMMANDS.filter((c) => c.name.toLowerCase().includes(q))
: COMMANDS.slice();
listEl.innerHTML = '';
matched.forEach((c, i) => {
const li = document.createElement('li');
li.className = 'cp-item' + (i === 0 ? ' is-active' : '');
li.setAttribute('role', 'option');
li.dataset.name = c.name;
li.innerHTML =
'<span class="cp-item__icon">' + c.icon + '</span>' +
'<span class="cp-item__name"></span>' +
'<span class="cp-item__hint">' + c.hint + '</span>';
// ユーザー入力由来でない固定文字列だが name は textContent で安全に
li.querySelector('.cp-item__name').textContent = c.name;
li.addEventListener('mousemove', () => setActive(i));
li.addEventListener('click', () => execAt(i));
listEl.appendChild(li);
});
items = Array.from(listEl.children);
active = 0;
if (emptyEl) emptyEl.hidden = items.length > 0;
};
// 選択ハイライトを更新
const setActive = (i) => {
if (!items.length) return;
active = (i + items.length) % items.length;
items.forEach((el, n) => el.classList.toggle('is-active', n === active));
items[active].scrollIntoView({ block: 'nearest' });
};
// 指定位置の候補を実行
const execAt = (i) => {
const el = items[i];
if (!el) return;
const name = el.dataset.name || '';
close();
if (resultEl) {
resultEl.textContent = '実行: ' + name;
resultEl.classList.add('is-hit');
}
showToast('▶ ' + name);
};
// トーストを一時表示
const showToast = (msg) => {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.hidden = false;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastEl.hidden = true; }, 1800);
};
const open = () => {
overlay.hidden = false;
input.value = '';
render();
input.focus();
};
const close = () => {
overlay.hidden = true;
};
if (openBtn) openBtn.addEventListener('click', open);
// 入力でフィルタ
input.addEventListener('input', render);
// パレット内のキー操作
overlay.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(active + 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive(active - 1); }
else if (e.key === 'Enter') { e.preventDefault(); execAt(active); }
else if (e.key === 'Escape') { e.preventDefault(); close(); }
});
// 背景クリックで閉じる
overlay.addEventListener('mousedown', (e) => {
if (e.target === overlay) close();
});
// ⌘K / Ctrl+K を iframe 内で捕捉(document レベル)
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
overlay.hidden ? open() : close();
}
});
}
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。