Tips & Tricks

Designing and Implementing Modal Dialogs: Claude Code 활용 가이드

designing and implementing modal dialogs: Claude Code 활용. 실용적인 코드 예시를 포함합니다.

모달・다이얼로그の설계原則

모달は사용자の주의を特定の操作に集中させる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>
  );
}

정리

Claude Code를 활용하면 HTML dialog要素を활용したアクセシブルな모달から、확인다이얼로그、コマンドパレットまで효율적으로구축할 수 있습니다。토스트알림との使い分けは토스트알림구현を、アクセシビリティの基本はアクセシビリティ가이드를 참고하세요.

dialog要素의 사양은MDN Web Docs - dialog를 확인하세요.

#Claude Code #modal #ダイアログ #React #accessibility