モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
ユーザーアクションを促す基本的なインタラクティブ要素。様々なスタイルとサイズに対応。
10種類のバリアントと5つのサイズで、あらゆる用途に対応
キーボード操作とスクリーンリーダーに完全対応
ローディング、無効化、ホバーなど多様な状態を表現
プロンプト例:
ユーザーのクリック率を最大化するボタンデザインシステムを作成してください。A/Bテスト機能、ヒートマップ連携、マイクロインタラクション、心理的トリガー(緊急性、希少性)の実装を含めてください。
プロンプト例:
全てのユーザーが使いやすいユニバーサルボタンコンポーネントを開発してください。音声フィードバック、触覚フィードバック、高コントラストモード、大きなタッチターゲット、複数の操作方法をサポートしてください。
プロンプト例:
コンテキストに応じて自動的に最適化されるスマートボタンを実装してください。ユーザーの行動履歴、デバイス特性、時間帯、画面サイズに基づいて、スタイル・サイズ・配置を動的に調整する機能を含めてください。
import React from 'react';
import type { BaseButtonProps } from '../types';
interface ButtonProps extends BaseButtonProps {
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
fullWidth = false,
loading = false,
disabled = false,
icon,
iconPosition = 'left',
type = 'button',
className = '',
onClick,
...rest
}) => {
// バリアントに基づくスタイル
const getVariantClasses = () => {
const variants = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900 focus:ring-gray-500',
success: 'bg-green-600 hover:bg-green-700 text-white focus:ring-green-500',
danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500',
warning: 'bg-yellow-500 hover:bg-yellow-600 text-white focus:ring-yellow-500',
info: 'bg-cyan-600 hover:bg-cyan-700 text-white focus:ring-cyan-500',
light: 'bg-white hover:bg-gray-100 text-gray-900 border border-gray-300 focus:ring-gray-500',
dark: 'bg-gray-900 hover:bg-gray-800 text-white focus:ring-gray-500',
ghost: 'bg-transparent hover:bg-gray-100 text-gray-700 focus:ring-gray-500',
link: 'bg-transparent hover:underline text-blue-600 focus:ring-blue-500 p-0',
};
return variants[variant as keyof typeof variants] || variants.primary;
};
// サイズに基づくスタイル
const getSizeClasses = () => {
const sizes = {
xs: 'text-xs px-2.5 py-1.5',
sm: 'text-sm px-3 py-2',
md: 'text-base px-4 py-2.5',
lg: 'text-lg px-5 py-3',
xl: 'text-xl px-6 py-3.5',
};
return variant === 'link' ? '' : sizes[size as keyof typeof sizes] || sizes.md;
};
// ローディングアイコン
const LoadingIcon = () => (
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
);
const baseClasses = variant === 'link'
? 'inline-flex items-center justify-center font-medium transition-all duration-200 focus:outline-none'
: 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
const widthClass = fullWidth ? 'w-full' : '';
const disabledClasses = disabled || loading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer';
const buttonClasses = `
${baseClasses}
${getVariantClasses()}
${getSizeClasses()}
${widthClass}
${disabledClasses}
${className}
`.trim();
const renderIcon = () => {
if (loading) return <LoadingIcon />;
if (icon) return <span className="flex-shrink-0">{icon}</span>;
return null;
};
return (
<button
type={type}
className={buttonClasses}
disabled={disabled || loading}
onClick={onClick}
{...rest}
>
{iconPosition === 'left' && renderIcon() && (
<span className={children ? 'mr-2' : ''}>{renderIcon()}</span>
)}
{children}
{iconPosition === 'right' && renderIcon() && (
<span className={children ? 'ml-2' : ''}>{renderIcon()}</span>
)}
</button>
);
};
// アイコンボタン
export const IconButton: React.FC<Omit<ButtonProps, 'children'> & { 'aria-label': string }> = ({
icon,
size = 'md',
...props
}) => {
// アイコンボタン用のサイズ調整
const getIconSizeClasses = () => {
const sizes = {
xs: 'p-1',
sm: 'p-1.5',
md: 'p-2',
lg: 'p-2.5',
xl: 'p-3',
};
return sizes[size as keyof typeof sizes] || sizes.md;
};
return (
<Button
{...props}
size={size}
className={`${getIconSizeClasses()} ${props.className || ''}`}
icon={icon}
>
{/* アイコンのみ、childrenなし */}
</Button>
);
};
// ボタンのデモコンポーネント
export const ButtonDemo: React.FC = () => {
return (
<div className="space-y-12">
{/* バリアント */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">バリアント</h3>
<div className="flex flex-wrap gap-3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="success">Success</Button>
<Button variant="danger">Danger</Button>
<Button variant="warning">Warning</Button>
<Button variant="info">Info</Button>
<Button variant="light">Light</Button>
<Button variant="dark">Dark</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
</div>
{/* サイズ */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">サイズ</h3>
<div className="flex items-center gap-3">
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
</div>
</div>
{/* 状態 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">状態</h3>
<div className="flex flex-wrap gap-3">
<Button>通常</Button>
<Button disabled>無効</Button>
<Button loading>読み込み中</Button>
</div>
</div>
{/* アイコン付き */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">アイコン付き</h3>
<div className="flex flex-wrap gap-3">
<Button
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 4v16m8-8H4" />
</svg>
}
>
追加
</Button>
<Button
variant="danger"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
}
iconPosition="right"
>
削除
</Button>
</div>
</div>
{/* アイコンボタン */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">アイコンボタン</h3>
<div className="flex items-center gap-3">
<IconButton
aria-label="設定"
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>
}
/>
<IconButton
aria-label="お気に入り"
variant="danger"
icon={
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
}
/>
</div>
</div>
{/* フルワイド */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">フルワイド</h3>
<div className="max-w-sm space-y-2">
<Button fullWidth>フルワイドボタン</Button>
<Button fullWidth variant="secondary">セカンダリフルワイド</Button>
</div>
</div>
</div>
);
};
export default Button;
// 基本的な使用例
import { Button, IconButton } from './button';
function App() {
return (
<div className="space-y-4">
{/* 基本的なボタン */}
<Button onClick={() => console.log('Clicked!')}>
クリックしてください
</Button>
{/* バリアントの使用 */}
<Button variant="primary">プライマリ</Button>
<Button variant="secondary">セカンダリ</Button>
<Button variant="success">成功</Button>
<Button variant="danger">危険</Button>
{/* サイズの変更 */}
<Button size="sm">小さいボタン</Button>
<Button size="lg">大きいボタン</Button>
{/* アイコン付きボタン */}
<Button
icon={<PlusIcon />}
iconPosition="left"
>
新規作成
</Button>
{/* ローディング状態 */}
<Button loading>
処理中...
</Button>
{/* 無効化 */}
<Button disabled>
利用不可
</Button>
{/* フルワイド */}
<Button fullWidth>
全幅ボタン
</Button>
{/* アイコンボタン */}
<IconButton
aria-label="設定"
icon={<SettingsIcon />}
variant="ghost"
/>
{/* リンクスタイル */}
<Button variant="link">
詳細を見る
</Button>
</div>
);
}