Designing and Implementing Modal Dialogs with Claude Code
Learn about designing and implementing modal dialogs using Claude Code. Includes practical code examples.
モーダル・ダイアログの設計原則
モーダルはユーザーの注意を特定の操作に集中させるUIパターンです。しかし、フォーカス管理やキーボード操作、スクリーンリーダー対応を怠ると、アクセシビリティの問題を引き起こします。Claude Codeを使えば、WAI-ARIA準拠のモーダルを正しく実装できます。
HTML dialog要素を使った実装
> HTML標準のdialog要素を使ったモーダルコンポーネントを作って。
> アクセシビリティとアニメーションに対応して。
import { useRef, useEffect } from 'react';
interface DialogProps {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function Dialog({ open, onClose, title, children }: DialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open) {
dialog.showModal();
} else {
dialog.close();
}
}, [open]);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
const handleClose = () => onClose();
dialog.addEventListener('close', handleClose);
const handleClick = (e: MouseEvent) => {
if (e.target === dialog) onClose();
};
dialog.addEventListener('click', handleClick);
return () => {
dialog.removeEventListener('close', handleClose);
dialog.removeEventListener('click', handleClick);
};
}, [onClose]);
return (
<dialog
ref={dialogRef}
aria-labelledby="dialog-title"
className="rounded-xl shadow-2xl p-0 backdrop:bg-black/50 backdrop:backdrop-blur-sm
max-w-lg w-full open:animate-fadeIn"
>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 id="dialog-title" className="text-xl font-bold">{title}</h2>
<button onClick={onClose} aria-label="Close"
className="rounded-full p-1 hover:bg-gray-100">✕</button>
</div>
{children}
</div>
</dialog>
);
}
確認ダイアログ
> 「Deleteしてよろしいですか?」のような確認ダイアログをPromiseベースで使えるようにして。
import { createRoot } from 'react-dom/client';
interface ConfirmOptions {
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: 'danger' | 'default';
}
function confirm(options: ConfirmOptions): Promise<boolean> {
return new Promise((resolve) => {
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
const handleClose = (result: boolean) => {
root.unmount();
container.remove();
resolve(result);
};
root.render(
<Dialog open={true} onClose={() => handleClose(false)} title={options.title}>
<p className="text-gray-600 mb-6">{options.message}</p>
<div className="flex justify-end gap-3">
<button onClick={() => handleClose(false)}
className="px-4 py-2 border rounded-lg hover:bg-gray-50">
{options.cancelLabel ?? 'キャンセル'}
</button>
<button onClick={() => handleClose(true)}
className={`px-4 py-2 rounded-lg text-white ${
options.variant === 'danger' ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'
}`}>
{options.confirmLabel ?? '確認'}
</button>
</div>
</Dialog>
);
});
}
// Usage example
async function handleDelete(id: string) {
const ok = await confirm({
title: 'Deleteの確認',
message: 'この項目をDeleteしてよろしいですか?この操作は取り消せません。',
confirmLabel: 'Deleteする',
variant: 'danger',
});
if (ok) await deleteItem(id);
}
コマンドパレット
function CommandPalette({ commands, onClose }: { commands: Command[]; onClose: () => void }) {
const [query, setQuery] = useState('');
const [activeIndex, setActiveIndex] = useState(0);
const filtered = commands.filter((c) =>
c.label.toLowerCase().includes(query.toLowerCase())
);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setActiveIndex((prev) => Math.min(prev + 1, filtered.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActiveIndex((prev) => Math.max(prev - 1, 0));
} else if (e.key === 'Enter' && filtered[activeIndex]) {
filtered[activeIndex].action();
onClose();
}
};
return (
<Dialog open={true} onClose={onClose} title="コマンドパレット">
<input
autoFocus
value={query}
onChange={(e) => { setQuery(e.target.value); setActiveIndex(0); }}
onKeyDown={handleKeyDown}
placeholder="コマンドを検索..."
className="w-full px-3 py-2 border rounded-lg mb-3"
role="combobox"
aria-expanded={true}
/>
<ul role="listbox" className="max-h-64 overflow-y-auto">
{filtered.map((cmd, i) => (
<li key={cmd.id} role="option" aria-selected={i === activeIndex}
onClick={() => { cmd.action(); onClose(); }}
className={`px-3 py-2 rounded cursor-pointer ${i === activeIndex ? 'bg-blue-100' : 'hover:bg-gray-50'}`}>
{cmd.label}
</li>
))}
</ul>
</Dialog>
);
}
Summary
Claude Codeを使えば、HTML dialog要素を活用したアクセシブルなモーダルから、確認ダイアログ、コマンドパレットまで効率的に構築できます。トースト通知との使い分けはトースト通知実装を、アクセシビリティの基本はアクセシビリティガイドを参照してください。
dialog要素の仕様はMDN Web Docs - dialogをご覧ください。
Free PDF: Claude Code Cheatsheet in 5 Minutes
Just enter your email and we'll send you the single-page A4 cheatsheet right away.
We handle your data with care and never send spam.
Level up your Claude Code workflow
50 battle-tested prompt templates you can copy-paste into Claude Code right now.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
7 CLAUDE.md Templates for Claude Code You Can Copy Into Real Projects
Copy-paste 7 practical CLAUDE.md templates for solo apps, content sites, APIs, teams, and legacy codebases, plus the failure cases to avoid.
Claude Code Approval and Sandbox Guide | Safe Daily Settings for Real Work
Learn how to split Claude Code actions into allow, ask, deny, and sandboxed workflows with working settings, hooks, and rollout examples.
Complete Beginner's Guide to Claude Code 2026 | 7 Steps from Zero to Production-Ready
A complete beginner's guide for first-time Claude Code users. From installation to integrating it into your real development workflow — covering every pitfall Masa ran into when starting out.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.