モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
情報を整理して表示するための構造化されたリストコンポーネント。順序付き、アイコン付き、インタラクティブなど多様な形式に対応。
要件定義とプランニング
UIデザインとプロトタイプ作成
実装とテスト
基本リスト、チェックリスト、説明リスト、タイムラインなど
クリック可能、選択可能、ドラッグ&ドロップ対応
アイコン、スタイル、レイアウトを自由に設定
プロンプト例:
AI搭載のインテリジェントタスクリストを作成してください。優先度の自動判定、期限管理、進捗の可視化、関連タスクのグループ化、自然言語での追加機能を実装してください。
プロンプト例:
長文コンテンツ用の高機能な目次リストを開発してください。スクロール追従、現在位置のハイライト、セクション間のスムーズな移動、読了時間の表示、ブックマーク機能を含めてください。
プロンプト例:
SNS風の無限スクロールリストコンポーネントを実装してください。リアルタイム更新、いいね・コメント機能、画像・動画のプレビュー、ユーザーアクションのアニメーション、最適化されたパフォーマンスを含めてください。
import React from 'react';
import type { BaseListProps } from '../types';
interface ListProps extends BaseListProps {
children: React.ReactNode;
className?: string;
}
interface ListItemProps {
children: React.ReactNode;
icon?: React.ReactNode;
active?: boolean;
disabled?: boolean;
onClick?: () => void;
className?: string;
}
export const List: React.FC<ListProps> = ({
children,
variant = 'default',
size = 'md',
ordered = false,
className = '',
}) => {
// サイズに基づくスタイル
const getSizeClasses = () => {
const sizes = {
sm: 'text-sm space-y-1',
md: 'text-base space-y-2',
lg: 'text-lg space-y-3',
};
return sizes[size as keyof typeof sizes] || sizes.md;
};
// バリアントに基づくスタイル
const getVariantClasses = () => {
const variants = {
default: '',
bordered: 'border border-gray-200 rounded-lg p-4',
striped: '[&>*:nth-child(odd)]:bg-gray-50',
hover: '[&>*]:hover:bg-gray-50',
};
return variants[variant as keyof typeof variants] || '';
};
const Component = ordered ? 'ol' : 'ul';
return (
<Component
className={`
${getSizeClasses()}
${getVariantClasses()}
${ordered ? 'list-decimal list-inside' : ''}
${className}
`}
>
{children}
</Component>
);
};
export const ListItem: React.FC<ListItemProps> = ({
children,
icon,
active = false,
disabled = false,
onClick,
className = '',
}) => {
const isInteractive = !!onClick;
const itemClasses = `
flex items-center px-3 py-2 rounded-lg transition-colors
${active ? 'bg-blue-50 text-blue-700' : ''}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${isInteractive && !disabled ? 'cursor-pointer hover:bg-gray-100' : ''}
${className}
`.trim();
const content = (
<>
{icon && (
<span className={`flex-shrink-0 mr-3 ${active ? 'text-blue-600' : 'text-gray-400'}`}>
{icon}
</span>
)}
<span className="flex-1">{children}</span>
</>
);
if (onClick && !disabled) {
return (
<li
className={itemClasses}
onClick={onClick}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{content}
</li>
);
}
return <li className={itemClasses}>{content}</li>;
};
// チェックリスト
export const CheckList: React.FC<{
items: Array<{
id: string;
label: string;
checked: boolean;
}>;
onToggle: (id: string) => void;
className?: string;
}> = ({ items, onToggle, className = '' }) => {
return (
<ul className={`space-y-2 ${className}`}>
{items.map((item) => (
<li key={item.id} className="flex items-center">
<input
type="checkbox"
id={item.id}
checked={item.checked}
onChange={() => onToggle(item.id)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label
htmlFor={item.id}
className={`ml-3 text-sm cursor-pointer ${
item.checked ? 'line-through text-gray-500' : 'text-gray-700'
}`}
>
{item.label}
</label>
</li>
))}
</ul>
);
};
// 説明リスト
export const DescriptionList: React.FC<{
items: Array<{
term: string;
description: string;
}>;
layout?: 'horizontal' | 'vertical';
className?: string;
}> = ({ items, layout = 'vertical', className = '' }) => {
if (layout === 'horizontal') {
return (
<dl className={`grid grid-cols-1 sm:grid-cols-2 gap-4 ${className}`}>
{items.map((item, index) => (
<div key={index} className="flex">
<dt className="font-medium text-gray-900 w-1/3">{item.term}</dt>
<dd className="text-gray-700 w-2/3">{item.description}</dd>
</div>
))}
</dl>
);
}
return (
<dl className={`space-y-4 ${className}`}>
{items.map((item, index) => (
<div key={index}>
<dt className="font-medium text-gray-900">{item.term}</dt>
<dd className="mt-1 text-gray-700">{item.description}</dd>
</div>
))}
</dl>
);
};
// タイムラインリスト
export const TimelineList: React.FC<{
items: Array<{
id: string;
title: string;
description?: string;
time: string;
icon?: React.ReactNode;
active?: boolean;
}>;
className?: string;
}> = ({ items, className = '' }) => {
return (
<div className={`relative ${className}`}>
{/* 垂直線 */}
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-300"></div>
<ul className="space-y-8">
{items.map((item, index) => (
<li key={item.id} className="relative flex items-start">
{/* ドット/アイコン */}
<div
className={`
relative z-10 flex items-center justify-center w-8 h-8 rounded-full
${item.active ? 'bg-blue-600' : 'bg-white border-2 border-gray-300'}
`}
>
{item.icon ? (
<span className={item.active ? 'text-white' : 'text-gray-600'}>
{item.icon}
</span>
) : (
<div className={`w-3 h-3 rounded-full ${item.active ? 'bg-white' : 'bg-gray-400'}`} />
)}
</div>
{/* コンテンツ */}
<div className="ml-6 flex-1">
<div className="flex items-center mb-1">
<h4 className={`font-medium ${item.active ? 'text-blue-600' : 'text-gray-900'}`}>
{item.title}
</h4>
<span className="ml-auto text-sm text-gray-500">{item.time}</span>
</div>
{item.description && (
<p className="text-sm text-gray-600">{item.description}</p>
)}
</div>
</li>
))}
</ul>
</div>
);
};
// リストのデモコンポーネント
export const ListDemo: React.FC = () => {
const [checkItems, setCheckItems] = React.useState([
{ id: '1', label: 'タスク1を完了する', checked: true },
{ id: '2', label: 'ミーティングに参加する', checked: false },
{ id: '3', label: 'レポートを提出する', checked: false },
]);
const toggleCheck = (id: string) => {
setCheckItems(items =>
items.map(item =>
item.id === id ? { ...item, checked: !item.checked } : item
)
);
};
return (
<div className="space-y-12">
{/* 基本的なリスト */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">基本的なリスト</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">順序なしリスト</h4>
<List>
<ListItem>項目 1</ListItem>
<ListItem>項目 2</ListItem>
<ListItem>項目 3</ListItem>
</List>
</div>
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">順序付きリスト</h4>
<List ordered>
<ListItem>最初のステップ</ListItem>
<ListItem>次のステップ</ListItem>
<ListItem>最後のステップ</ListItem>
</List>
</div>
</div>
</div>
{/* アイコン付きリスト */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">アイコン付きリスト</h3>
<List>
<ListItem
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
>
成功:タスクが完了しました
</ListItem>
<ListItem
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
}
>
警告:設定を確認してください
</ListItem>
<ListItem
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
>
情報:新機能が利用可能です
</ListItem>
</List>
</div>
{/* インタラクティブリスト */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">インタラクティブリスト</h3>
<List variant="bordered">
<ListItem
onClick={() => alert('ダッシュボードをクリック')}
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
}
active
>
ダッシュボード
</ListItem>
<ListItem
onClick={() => alert('プロジェクトをクリック')}
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
}
>
プロジェクト
</ListItem>
<ListItem
onClick={() => alert('無効化されています')}
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
}
disabled
>
設定(無効)
</ListItem>
</List>
</div>
{/* バリアント */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">バリアント</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">ストライプ</h4>
<List variant="striped">
<ListItem>奇数行は背景色付き</ListItem>
<ListItem>偶数行は通常の背景</ListItem>
<ListItem>奇数行は背景色付き</ListItem>
<ListItem>偶数行は通常の背景</ListItem>
</List>
</div>
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">ホバー効果</h4>
<List variant="hover">
<ListItem>マウスを乗せると</ListItem>
<ListItem>背景色が変わります</ListItem>
<ListItem>視覚的フィードバック</ListItem>
</List>
</div>
</div>
</div>
{/* チェックリスト */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">チェックリスト</h3>
<CheckList items={checkItems} onToggle={toggleCheck} />
</div>
{/* 説明リスト */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">説明リスト</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">垂直レイアウト</h4>
<DescriptionList
items={[
{ term: 'HTML', description: 'ウェブページの構造を定義' },
{ term: 'CSS', description: 'スタイルとレイアウトを制御' },
{ term: 'JavaScript', description: 'インタラクティブな動作を実装' },
]}
/>
</div>
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">水平レイアウト</h4>
<DescriptionList
layout="horizontal"
items={[
{ term: 'React', description: 'UIライブラリ' },
{ term: 'Vue', description: 'プログレッシブフレームワーク' },
{ term: 'Angular', description: 'フルスタックフレームワーク' },
]}
/>
</div>
</div>
</div>
{/* タイムライン */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">タイムライン</h3>
<TimelineList
items={[
{
id: '1',
title: 'プロジェクト開始',
description: '要件定義とプランニング',
time: '9:00',
active: true,
},
{
id: '2',
title: 'デザイン完了',
description: 'UIデザインとプロトタイプ作成',
time: '11:30',
},
{
id: '3',
title: '開発フェーズ',
description: '実装とテスト',
time: '14:00',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
),
},
{
id: '4',
title: 'リリース予定',
time: '18:00',
},
]}
/>
</div>
</div>
);
};
export default List;
// 基本的な使用例
import { List, ListItem, CheckList, DescriptionList, TimelineList } from './list';
function App() {
const [checkItems, setCheckItems] = useState([
{ id: '1', label: 'タスク1', checked: true },
{ id: '2', label: 'タスク2', checked: false },
]);
return (
<div>
{/* 基本的なリスト */}
<List>
<ListItem>項目 1</ListItem>
<ListItem>項目 2</ListItem>
<ListItem>項目 3</ListItem>
</List>
{/* 順序付きリスト */}
<List ordered>
<ListItem>ステップ 1</ListItem>
<ListItem>ステップ 2</ListItem>
<ListItem>ステップ 3</ListItem>
</List>
{/* アイコン付きリスト */}
<List>
<ListItem icon={<CheckIcon />}>
完了したタスク
</ListItem>
<ListItem icon={<AlertIcon />}>
注意が必要
</ListItem>
</List>
{/* インタラクティブリスト */}
<List variant="bordered">
<ListItem
onClick={() => navigate('/dashboard')}
icon={<HomeIcon />}
active
>
ダッシュボード
</ListItem>
<ListItem
onClick={() => navigate('/projects')}
icon={<FolderIcon />}
>
プロジェクト
</ListItem>
</List>
{/* バリアント */}
<List variant="striped">
<ListItem>ストライプ表示</ListItem>
<ListItem>交互に背景色</ListItem>
</List>
{/* チェックリスト */}
<CheckList
items={checkItems}
onToggle={(id) => {
setCheckItems(items =>
items.map(item =>
item.id === id ? { ...item, checked: !item.checked } : item
)
);
}}
/>
{/* 説明リスト */}
<DescriptionList
items={[
{ term: 'React', description: 'UIライブラリ' },
{ term: 'TypeScript', description: '型安全なJavaScript' },
]}
/>
{/* タイムライン */}
<TimelineList
items={[
{
id: '1',
title: 'プロジェクト開始',
description: '初期計画',
time: '9:00',
active: true,
},
{
id: '2',
title: '開発フェーズ',
time: '14:00',
},
]}
/>
</div>
);
}