React/Next.jsでメール管理システムのUIを改善した話 〜ダイアログからスライドパネルへの移行〜
概要
メール管理システムの検索UIにおいて、ユーザビリティの課題を解決するため、ダイアログ形式からスライドパネル形式への移行を実施しました。この記事では、なぜこの変更が必要だったのか、どのように実装したのか、そして実装過程で学んだことを共有します。
解決したかった課題
私たちのメール管理システムでは、以下のような UX 上の課題がありました:
Before: 従来のUI構成
画面構成:
- メール一覧テーブル(全画面表示)
- 詳細表示はモーダルダイアログ(背景が暗くなり操作不可)
- 各行の右端に小さな虫眼鏡アイコンボタン
テーブル列構成:
アイコン | 件名 | 送信者 | 顧客 | サイズ | 日時 | 操作 |
---|---|---|---|---|---|---|
📎 | 案件の相談 | 田中 | A社 | 2KB | 09/04 | 👁️ |
• | 面談依頼 | 佐藤 | B社 | 1KB | 09/03 | 👁️ |
課題点
- コンテキストの喪失: モーダルダイアログを開くと、背後のメール一覧が操作できなくなる
- 比較の困難さ: 複数のメールを素早く比較したいのに、いちいちダイアログを閉じる必要がある
- 情報の冗長性: テーブルに不要な列(顧客・サイズ)が多く、スペースが有効活用されていない
- 操作の煩雑さ: 小さな虫眼鏡アイコンをクリックする必要があり、ターゲットが小さく操作しづらい
技術スタック
- フレームワーク: Next.js 14 (App Router)
- UI ライブラリ: React 18
- スタイリング: Tailwind CSS
- コンポーネント: shadcn/ui
- 型安全性: TypeScript
- 状態管理: React Hooks (useState, useCallback, useMemo)
実装内容
After: 改善後のUI構成
画面構成:
- 画面を 6:4 の比率で分割
- 左側: メール一覧(常に操作可能)
- 右側: 詳細パネル(スライドイン表示)
- オーバーレイなし(背景は操作可能のまま)
改善されたテーブル列構成:
アイコン | 件名 | 送信者 | 日時 |
---|---|---|---|
📎 | 案件の相談 | 田中 | 09/04 |
面談依頼 | 佐藤 | 09/03 |
改善ポイント
- ✅ 行全体がクリック可能 - 小さなアイコンではなく行全体がクリックターゲット
- ✅ リストとパネルの同時表示 - 6:4の比率で画面分割し、両方を同時に操作可能
- ✅ 選択状態の可視化 - 選択中のメールは左側に青いボーダーと背景色で強調
- ✅ 不要な列を削除 - 顧客・サイズ列を削除してスペース効率化
- ✅ シンプルなフィルター - 案件・人材の2つのチェックボックスのみ
1. ダイアログからスライドパネルへの変更
Before: モーダルダイアログ
// 従来のダイアログ実装
export function EmailPreviewDialog({
email,
open,
onOpenChange,
}: EmailPreviewDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[80vh]">
{/* メール詳細内容 */}
</DialogContent>
</Dialog>
)
}
After: スライドパネル
// 新しいスライドパネル実装
export function EmailPreviewPanel({
email,
emailDetail,
open,
onOpenChange,
}: EmailPreviewPanelProps) {
return (
<div className="h-full">
<div className="bg-background border rounded-md shadow-sm h-[calc(100vh-120px)]">
<div className="flex flex-col h-full">
{/* ヘッダー */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold">メール詳細</h2>
<Button variant="ghost" size="icon" onClick={() => onOpenChange(false)}>
<X className="h-4 w-4" />
</Button>
</div>
{/* スクロール可能なコンテンツ */}
<ScrollArea className="flex-1 p-4">
{/* メール詳細内容 */}
</ScrollArea>
</div>
</div>
</div>
)
}
2. レスポンシブなグリッドレイアウトの実装
パネルの開閉に応じて、動的にレイアウトを調整する仕組みを実装しました:
// 動的なグリッドレイアウト
<div className="grid grid-cols-10 gap-4">
{/* メール一覧 */}
<div className={`transition-all duration-300 ${
previewOpen ? 'col-span-6' : 'col-span-10'
}`}>
<EmailList />
</div>
{/* プレビューパネル */}
{previewOpen && (
<div className="col-span-4">
<EmailPreviewPanel />
</div>
)}
</div>
3. テーブル行全体をクリック可能に
ユーザビリティ向上のため、小さなアイコンボタンではなく、行全体をクリック可能にしました:
// 行全体をクリック可能に
<TableRow
key={email.id}
className={`h-12 cursor-pointer transition-all duration-200 ${
selectedEmail?.id === email.id
? 'bg-primary/15 border-l-4 border-l-primary'
: 'hover:bg-muted/60'
}`}
onClick={() => handlePreviewOpen(email)}
>
{/* テーブルセルの内容 */}
</TableRow>
4. Server Actions を使った効率的なデータフェッチ
完全なメール内容を必要な時だけ取得する仕組みを実装:
// Server Action
export async function fetchEmailDetail(
emailId: string,
organizationId?: string
): Promise<EmailDetail | null> {
const emailRepository = createEmailRepository()
const emailDetail = await emailRepository.findById(emailId)
// セキュリティチェック
if (emailDetail?.organizationId !== organizationId) {
return null
}
return emailDetail
}
// クライアント側での使用
const handlePreviewOpen = async (email: Email) => {
setPreviewEmail(email)
setPreviewOpen(true)
setIsLoadingDetail(true)
try {
const detail = await fetchEmailDetail(email.id, organizationId)
setPreviewEmailDetail(detail)
} catch (error) {
console.error('Failed to fetch email detail:', error)
} finally {
setIsLoadingDetail(false)
}
}
5. 無限スクロール時の重複防止
// 重複を防ぐロジック
setEmails((prev) => {
const existingIds = new Set(prev.map(email => email.id))
const newEmails = result.emails.filter(
email => !existingIds.has(email.id)
)
return [...prev, ...newEmails]
})
操作フローの比較
Before: 従来の操作フロー
- 小さな虫眼鏡アイコンをクリック(ターゲットが小さい)
- ダイアログが表示され、背景が暗くなる
- この時点で一覧は操作不可
- 次のメールを見るには、一度ダイアログを閉じる必要がある
- 手順1に戻る
After: 改善後の操作フロー
- メールの行全体をクリック(ターゲットが大きい)
- 右側にパネルがスライド表示
- 一覧は引き続き操作可能
- 別のメールをクリックすると、パネル内容が即座に切り替わる
- 比較しながら素早くメールを確認可能
学んだこと
1. オーバーレイの功罪
最初はスライドパネルにもオーバーレイを付けていましたが、これがユーザーの「複数のメールを比較したい」というニーズを阻害していることに気づきました。オーバーレイを削除することで、リストとパネルの同時操作が可能になり、UXが大幅に向上しました。
2. TypeScript の型定義の重要性
NextAuth のセッション型を拡張する際、型定義ファイルが .gitignore
に含まれていて苦労しました:
// types/next-auth.d.ts
declare module 'next-auth' {
interface Session {
user: {
organizationId?: string
} & DefaultSession['user']
}
}
3. CSS クラスの特異性
選択中のメールに左側のボーダーを表示する際、border-primary
だけだと下部にもボーダーが表示されてしまいました。border-l-primary
と明示的に指定することで解決:
/* 誤った実装 */
.selected {
@apply border-l-4 border-primary; /* 全方向にボーダーが適用される可能性 */
}
/* 正しい実装 */
.selected {
@apply border-l-4 border-l-primary; /* 左側のみにボーダー */
}
4. Server Component と Client Component の境界
Server Actions を使うことで、必要な時だけデータをフェッチし、初期表示を高速化できました。ただし、組織IDなどのセキュリティ情報の扱いには注意が必要です。
他の開発者への応用可能性
この実装パターンは、以下のようなケースで応用できます:
- マスター・ディテール UI: 一覧と詳細を同時に表示したい場合
- 比較検討が必要な UI: 複数のアイテムを素早く切り替えて確認したい場合
- コンテキスト維持が重要な UI: 背景の情報を見ながら操作したい場合
実装のポイント
- レスポンシブなグリッドシステムを使う
- アニメーション(
transition-all
)で変化を滑らかに - 選択状態を視覚的に明確にする
- データフェッチは必要最小限に
まとめ
ダイアログからスライドパネルへの移行は、単なるUI の変更以上の価値がありました。ユーザーの実際の使い方を理解し、それに合わせてUIを最適化することの重要性を改めて認識しました。
技術的には複雑な実装ではありませんが、ユーザビリティの観点から大きな改善となりました。特に、「複数のメールを素早く確認したい」というユーザーのニーズに応えることができ、作業効率が大幅に向上したという feedback を得ています。
今後の展望
- キーボードショートカットの実装(矢印キーでメール選択)
- パネルのリサイズ機能
- メール本文の検索ハイライト強化
- モバイル対応の最適化
この記事で紹介したコードは、実際のプロダクションコードを簡略化したものです。エラーハンドリングやセキュリティチェックなど、実際の実装では追加の考慮事項があります。
関連技術: React, Next.js, TypeScript, Tailwind CSS, Server Actions, shadcn/ui
筆者: 91works開発チーム
Discussion