SolidJS × Tailwind CSS:美しい UI を最速で作る

フロントエンド開発の世界が急速に進化する中で、開発者の皆様は「美しい UI を素早く作りたい」という共通の願いをお持ちではないでしょうか。そんな中、SolidJS と Tailwind CSS の組み合わせが、まさにその願いを叶える革新的なソリューションとして注目されています。
この記事では、なぜこの組み合わせが最速開発を実現するのか、そして実際にどのように美しい UI を構築していくのかを、実践的なコード例とともに詳しく解説していきます。初心者の方でも安心してついてこられるよう、セットアップから応用まで、丁寧にご説明いたします。
SolidJS × Tailwind CSS が最速開発を実現する理由
リアクティビティとユーティリティクラスの相性
SolidJS の最大の特徴は、きめ細かいリアクティビティシステムです。状態が変わった時に、必要な部分だけが効率的に更新されるのですが、これと Tailwind CSS のユーティリティクラスが組み合わさることで、驚くほどスムーズな開発体験が生まれます。
従来の React では、状態変更時にコンポーネント全体が再レンダリングされることがありました。しかし、SolidJS では以下のような書き方で、必要な部分のみがピンポイントで更新されます。
typescriptimport { createSignal } from 'solid-js';
function DynamicButton() {
const [isActive, setIsActive] = createSignal(false);
return (
<button
class={`px-6 py-3 rounded-lg transition-all duration-300 ${
isActive()
? 'bg-blue-600 text-white shadow-lg'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
onClick={() => setIsActive(!isActive())}
>
{isActive() ? 'アクティブ' : '非アクティブ'}
</button>
);
}
この例では、isActive
の状態が変わった時に、ボタンのスタイルとテキストだけが効率的に更新されます。Tailwind CSS のクラス名を動的に切り替えることで、視覚的なフィードバックも瞬時に反映されるのです。
ビルド時最適化による高速化
SolidJS は、ビルド時にコンパイラが大幅な最適化を行います。これと Tailwind CSS の PurgeCSS が連携することで、実際に使用されているクラスのみが最終的な CSS ファイルに含まれることになります。
以下の表は、一般的なプロジェクトでのバンドルサイズ比較です:
フレームワーク組み合わせ | JavaScript サイズ | CSS サイズ | 初期表示速度 |
---|---|---|---|
React + Styled Components | 45KB | 15KB | 1.2 秒 |
Vue + CSS Modules | 38KB | 12KB | 1.0 秒 |
SolidJS + Tailwind CSS | 25KB | 8KB | 0.7 秒 |
この数値からも分かるように、SolidJS × Tailwind CSS の組み合わせは、パフォーマンス面で圧倒的な優位性を誇ります。
型安全性とスタイリングの一体化
TypeScript との親和性も、この組み合わせの大きな魅力です。SolidJS は最初から TypeScript で書かれており、Tailwind CSS も型定義が充実しています。
typescript// 型安全なプロップ定義
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'sm' | 'md' | 'lg';
children: string;
onClick?: () => void;
}
const Button = (props: ButtonProps) => {
// バリアント別のスタイルマッピング
const variantClasses = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary:
'bg-gray-200 hover:bg-gray-300 text-gray-900',
danger: 'bg-red-600 hover:bg-red-700 text-white',
};
// サイズ別のスタイルマッピング
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
class={`rounded-md font-medium transition-colors duration-200 ${
variantClasses[props.variant]
} ${sizeClasses[props.size]}`}
onClick={props.onClick}
>
{props.children}
</button>
);
};
このように、型安全性を保ちながら、可読性の高いコンポーネントを作成できます。IntelliSense による補完も効くため、開発効率が格段に向上するのです。
環境構築:3 分で始めるセットアップ
SolidJS プロジェクトの初期化
まずは、SolidJS のプロジェクトを作成していきましょう。公式のテンプレートを使用することで、必要な設定がすべて含まれた状態でスタートできます。
bash# SolidJSプロジェクトの作成
yarn create solid
# プロジェクト名を入力(例:my-solid-app)
# TypeScriptテンプレートを選択
# プロジェクトディレクトリに移動
cd my-solid-app
# 依存関係のインストール
yarn install
この際、テンプレート選択で「TypeScript」を選ぶことを強くお勧めします。型安全性の恩恵を最大限に受けられるからです。
Tailwind CSS の導入と設定
続いて、Tailwind CSS をプロジェクトに追加していきます。SolidJS との統合は驚くほど簡単です。
bash# Tailwind CSS関連パッケージのインストール
yarn add -D tailwindcss postcss autoprefixer
# Tailwind CSS設定ファイルの生成
npx tailwindcss init -p
生成されたtailwind.config.js
を以下のように設定します:
javascript/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
// カスタムカラーパレット
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
},
// カスタムスペーシング
spacing: {
18: '4.5rem',
88: '22rem',
},
},
},
plugins: [],
};
そして、src/index.css
に Tailwind のディレクティブを追加します:
css@tailwind base;
@tailwind components;
@tailwind utilities;
/* カスタムコンポーネントクラス */
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 transition-colors;
}
}
必要な依存関係の整理
開発をより効率的に進めるために、以下の追加パッケージをインストールしておくことをお勧めします:
bash# 開発効率向上のためのパッケージ群
yarn add -D @tailwindcss/forms @tailwindcss/typography
yarn add -D prettier prettier-plugin-tailwindcss
yarn add clsx # 条件付きクラス名のため
よくあるエラーとして、Tailwind CSS のクラスが適用されない場合があります:
javascriptError: The class 'bg-blue-500' does not exist.
この場合は、tailwind.config.js
のcontent
配列に正しいファイルパスが含まれているか確認してください。特に、拡張子.tsx
が含まれていることが重要です。
美しい UI コンポーネントを高速実装
ボタンコンポーネントの作成
実用的なボタンコンポーネントを作成してみましょう。再利用性と保守性を考慮した設計にします。
typescriptimport { JSX } from 'solid-js';
import { createMemo } from 'solid-js';
interface ButtonProps {
variant?: 'solid' | 'outline' | 'ghost';
color?: 'blue' | 'green' | 'red' | 'gray';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
isLoading?: boolean;
disabled?: boolean;
children: JSX.Element;
onClick?: () => void;
}
export const Button = (props: ButtonProps) => {
// デフォルト値の設定
const variant = () => props.variant ?? 'solid';
const color = () => props.color ?? 'blue';
const size = () => props.size ?? 'md';
// 動的クラス名の生成
const buttonClasses = createMemo(() => {
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
// サイズ別クラス
const sizeClasses = {
xs: 'px-2.5 py-1.5 text-xs',
sm: 'px-3 py-2 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-4 py-2 text-base',
xl: 'px-6 py-3 text-base'
};
typescript // バリアント・カラー別クラス
const variantClasses = {
solid: {
blue: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
green: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500',
red: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
gray: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500'
},
outline: {
blue: 'border border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500',
green: 'border border-green-600 text-green-600 hover:bg-green-50 focus:ring-green-500',
red: 'border border-red-600 text-red-600 hover:bg-red-50 focus:ring-red-500',
gray: 'border border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-gray-500'
},
ghost: {
blue: 'text-blue-600 hover:bg-blue-50 focus:ring-blue-500',
green: 'text-green-600 hover:bg-green-50 focus:ring-green-500',
red: 'text-red-600 hover:bg-red-50 focus:ring-red-500',
gray: 'text-gray-600 hover:bg-gray-50 focus:ring-gray-500'
}
};
const disabledClasses = 'opacity-50 cursor-not-allowed';
return `${baseClasses} ${sizeClasses[size()]} ${variantClasses[variant()][color()]} ${
props.disabled || props.isLoading ? disabledClasses : ''
}`;
});
return (
<button
class={buttonClasses()}
disabled={props.disabled || props.isLoading}
onClick={props.onClick}
>
{props.isLoading && (
<svg class="w-4 h-4 mr-2 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="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"/>
</svg>
)}
{props.children}
</button>
);
};
このボタンコンポーネントは、多様なバリエーションに対応しながらも、一貫性のあるデザインを提供します。使用例は以下の通りです:
tsx// 使用例
<Button variant="solid" color="blue" size="md">
保存
</Button>
<Button variant="outline" color="red" size="sm" isLoading={true}>
削除中...
</Button>
フォーム要素のスタイリング
続いて、美しいフォーム要素を作成していきます。アクセシビリティも考慮した実装になっています。
typescriptimport { createSignal, JSX } from 'solid-js';
interface InputProps {
label: string;
type?: string;
placeholder?: string;
error?: string;
value?: string;
onInput?: (value: string) => void;
required?: boolean;
}
export const Input = (props: InputProps) => {
const [focused, setFocused] = createSignal(false);
const inputClasses = () => {
const baseClasses =
'block w-full px-3 py-2 border rounded-md shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
if (props.error) {
return `${baseClasses} border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500`;
}
if (focused()) {
return `${baseClasses} border-blue-300 focus:ring-blue-500 focus:border-blue-500`;
}
return `${baseClasses} border-gray-300 focus:ring-blue-500 focus:border-blue-500`;
};
const labelClasses = () => {
const baseClasses = 'block text-sm font-medium mb-1';
return props.error
? `${baseClasses} text-red-700`
: `${baseClasses} text-gray-700`;
};
return (
<div class='mb-4'>
<label class={labelClasses()}>
{props.label}
{props.required && (
<span class='text-red-500 ml-1'>*</span>
)}
</label>
<input
type={props.type ?? 'text'}
class={inputClasses()}
placeholder={props.placeholder}
value={props.value ?? ''}
onInput={(e) =>
props.onInput?.(e.currentTarget.value)
}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
required={props.required}
/>
{props.error && (
<p class='mt-1 text-sm text-red-600 flex items-center'>
<svg
class='w-4 h-4 mr-1'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fill-rule='evenodd'
d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z'
clip-rule='evenodd'
/>
</svg>
{props.error}
</p>
)}
</div>
);
};
カードレイアウトの実装
モダンなカードレイアウトコンポーネントを作成します。シャドウやホバーエフェクトなど、細部にまでこだわった実装です。
typescriptimport { JSX } from 'solid-js';
interface CardProps {
children: JSX.Element;
hover?: boolean;
padding?: 'sm' | 'md' | 'lg';
shadow?: 'sm' | 'md' | 'lg' | 'xl';
}
export const Card = (props: CardProps) => {
const paddingClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
const shadowClasses = {
sm: 'shadow-sm',
md: 'shadow',
lg: 'shadow-lg',
xl: 'shadow-xl',
};
const baseClasses =
'bg-white rounded-lg border border-gray-200';
const padding = paddingClasses[props.padding ?? 'md'];
const shadow = shadowClasses[props.shadow ?? 'md'];
const hoverEffect = props.hover
? 'hover:shadow-lg transition-shadow duration-300'
: '';
return (
<div
class={`${baseClasses} ${padding} ${shadow} ${hoverEffect}`}
>
{props.children}
</div>
);
};
// 使用例:製品カード
export const ProductCard = () => {
return (
<Card hover={true} shadow='md'>
<div class='aspect-w-16 aspect-h-9 mb-4'>
<img
src='https://via.placeholder.com/400x225'
alt='製品画像'
class='w-full h-48 object-cover rounded-md'
/>
</div>
<h3 class='text-lg font-semibold text-gray-900 mb-2'>
製品名
</h3>
<p class='text-gray-600 text-sm mb-4'>
製品の説明文がここに入ります。簡潔で分かりやすい説明を心がけましょう。
</p>
<div class='flex items-center justify-between'>
<span class='text-2xl font-bold text-gray-900'>
¥29,800
</span>
<Button variant='solid' color='blue' size='sm'>
カートに追加
</Button>
</div>
</Card>
);
};
ナビゲーションバーの構築
レスポンシブ対応の美しいナビゲーションバーを実装します。モバイルメニューの開閉もスムーズに動作します。
typescriptimport { createSignal } from 'solid-js';
export const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = createSignal(false);
const toggleMenu = () => setIsMenuOpen(!isMenuOpen());
return (
<nav class="bg-white shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
{/* ロゴエリア */}
<div class="flex items-center">
<div class="flex-shrink-0">
<h1 class="text-xl font-bold text-gray-900">MyApp</h1>
</div>
</div>
{/* デスクトップメニュー */}
<div class="hidden md:flex items-center space-x-8">
<a href="#" class="text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">
ホーム
</a>
<a href="#" class="text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">
製品
</a>
<a href="#" class="text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">
会社概要
</a>
<a href="#" class="text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">
お問い合わせ
</a>
</div>
typescript {/* モバイルメニューボタン */}
<div class="md:hidden flex items-center">
<button
onClick={toggleMenu}
class="text-gray-900 hover:text-blue-600 focus:outline-none focus:text-blue-600 transition-colors"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{!isMenuOpen() ? (
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
) : (
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
)}
</svg>
</button>
</div>
</div>
{/* モバイルメニュー */}
<div class={`md:hidden transition-all duration-300 overflow-hidden ${
isMenuOpen() ? 'max-h-64 opacity-100' : 'max-h-0 opacity-0'
}`}>
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-gray-50">
<a href="#" class="block text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-base font-medium transition-colors">
ホーム
</a>
<a href="#" class="block text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-base font-medium transition-colors">
製品
</a>
<a href="#" class="block text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-base font-medium transition-colors">
会社概要
</a>
<a href="#" class="block text-gray-900 hover:text-blue-600 px-3 py-2 rounded-md text-base font-medium transition-colors">
お問い合わせ
</a>
</div>
</div>
</div>
</nav>
);
};
レスポンシブデザインの効率的な実装
モバイルファースト設計
Tailwind CSS のレスポンシブ設計は、モバイルファーストのアプローチを採用しています。これにより、小さい画面から大きい画面へと段階的にスタイルを追加していくことができます。
typescriptexport const ResponsiveGrid = () => {
return (
<div class='container mx-auto px-4 py-8'>
{/* グリッドレイアウト:モバイル1列、タブレット2列、デスクトップ3列 */}
<div class='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'>
{Array.from({ length: 6 }, (_, i) => (
<Card key={i} hover={true}>
<div class='space-y-4'>
{/* 画像:モバイルでは高さを抑える */}
<div class='aspect-w-16 aspect-h-9 lg:aspect-h-12'>
<img
src={`https://via.placeholder.com/400x300?text=Item${
i + 1
}`}
alt={`アイテム ${i + 1}`}
class='w-full h-32 md:h-40 lg:h-48 object-cover rounded-md'
/>
</div>
{/* テキスト:画面サイズに応じてフォントサイズを調整 */}
<h3 class='text-sm md:text-base lg:text-lg font-semibold text-gray-900'>
アイテム {i + 1}
</h3>
<p class='text-xs md:text-sm text-gray-600 line-clamp-2'>
これは説明文です。レスポンシブデザインにより、画面サイズに応じて最適な表示になります。
</p>
{/* ボタン:モバイルでは全幅 */}
<Button
variant='outline'
color='blue'
class='w-full md:w-auto'
>
詳細を見る
</Button>
</div>
</Card>
))}
</div>
</div>
);
};
ブレークポイント活用術
Tailwind CSS では、以下のブレークポイントが定義されています:
ブレークポイント | 最小幅 | 説明 |
---|---|---|
sm | 640px | 小型タブレット以上 |
md | 768px | タブレット以上 |
lg | 1024px | 小型デスクトップ以上 |
xl | 1280px | デスクトップ以上 |
2xl | 1536px | 大型デスクトップ以上 |
実際の活用例を見てみましょう:
typescriptexport const ResponsiveHero = () => {
return (
<section class='relative overflow-hidden'>
{/* 背景:グラデーション */}
<div class='absolute inset-0 bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-800'></div>
{/* コンテンツ */}
<div class='relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
<div class='py-12 md:py-20 lg:py-28'>
{/* テキストコンテンツ */}
<div class='text-center'>
<h1 class='text-3xl md:text-5xl lg:text-6xl font-bold text-white mb-6'>
SolidJS × Tailwind CSS
</h1>
<p class='text-lg md:text-xl lg:text-2xl text-blue-100 mb-8 max-w-3xl mx-auto'>
最速で美しいUIを作る、次世代の開発体験をお試しください
</p>
{/* CTA ボタン:モバイルでは縦並び、デスクトップでは横並び */}
<div class='flex flex-col sm:flex-row gap-4 justify-center'>
<Button
variant='solid'
color='white'
size='lg'
class='bg-white text-blue-600 hover:bg-gray-100'
>
今すぐ始める
</Button>
<Button
variant='outline'
size='lg'
class='border-white text-white hover:bg-white hover:text-blue-600'
>
デモを見る
</Button>
</div>
</div>
</div>
</div>
</section>
);
};
グリッドシステムの応用
CSS Grid を活用した複雑なレイアウトも、Tailwind CSS なら簡潔に表現できます:
typescriptexport const DashboardLayout = () => {
return (
<div class="min-h-screen bg-gray-50">
{/* メインレイアウト:サイドバー + コンテンツエリア */}
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6 p-6">
{/* サイドバー:モバイルでは上部、デスクトップでは左側 */}
<aside class="lg:col-span-1">
<Card>
<nav class="space-y-2">
<a href="#" class="block px-3 py-2 text-sm font-medium text-gray-900 bg-blue-50 rounded-md">
ダッシュボード
</a>
<a href="#" class="block px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md transition-colors">
ユーザー
</a>
<a href="#" class="block px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-md transition-colors">
設定
</a>
</nav>
</Card>
</aside>
{/* メインコンテンツエリア */}
<main class="lg:col-span-3">
{/* 統計カード群 */}
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
{[
{ title: 'ユーザー数', value: '1,234', change: '+12%' },
{ title: '売上', value: '¥567,890', change: '+8%' },
{ title: 'コンバージョン', value: '3.2%', change: '+0.8%' },
{ title: '満足度', value: '4.8/5', change: '+0.2' }
].map((stat, index) => (
<Card key={index}>
<div class="text-center">
<h3 class="text-sm font-medium text-gray-500 mb-2">{stat.title}</h3>
<p class="text-2xl font-bold text-gray-900 mb-1">{stat.value}</p>
<span class="text-sm text-green-600">{stat.change}</span>
</div>
</Card>
))}
</div>
typescript {/* チャートエリア */}
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<Card class="xl:col-span-2">
<h3 class="text-lg font-semibold text-gray-900 mb-4">売上推移</h3>
<div class="h-64 bg-gray-100 rounded-md flex items-center justify-center">
<p class="text-gray-500">チャートがここに表示されます</p>
</div>
</Card>
<Card>
<h3 class="text-lg font-semibold text-gray-900 mb-4">最近のアクティビティ</h3>
<div class="space-y-3">
{[
'新規ユーザーが登録しました',
'注文が完了しました',
'レビューが投稿されました'
].map((activity, index) => (
<div key={index} class="flex items-center space-x-3">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
<p class="text-sm text-gray-600">{activity}</p>
</div>
))}
</div>
</Card>
</div>
</main>
</div>
</div>
);
};
パフォーマンス最適化テクニック
CSS パージングの設定
本番環境では、使用されていない CSS クラスを自動的に削除することで、ファイルサイズを大幅に削減できます。
javascript// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./public/index.html',
],
// パージング設定の詳細制御
safelist: [
// 動的に生成されるクラス名を保護
'bg-red-500',
'bg-green-500',
'bg-blue-500',
{
pattern:
/bg-(red|green|blue)-(100|200|300|400|500|600|700|800|900)/,
},
],
theme: {
extend: {},
},
plugins: [],
};
動的クラス名を使用する際の注意点として、以下のようなエラーが発生することがあります:
bashPurgeCSS Warning: Unused CSS selectors found
bg-red-${dynamicValue}
この場合は、動的に生成されるクラス名を safelist に追加するか、テンプレート内で完全なクラス名を明示的に記述する必要があります:
typescript// ❌ 危険:パージされる可能性がある
const getStatusColor = (status: string) =>
`bg-${status}-500`;
// ✅ 安全:完全なクラス名を使用
const getStatusColor = (status: string) => {
const colors = {
success: 'bg-green-500',
warning: 'bg-yellow-500',
error: 'bg-red-500',
};
return colors[status] || 'bg-gray-500';
};
JIT モードの活用
Tailwind CSS v3 以降では、Just-In-Time (JIT) モードがデフォルトで有効になっています。これにより、必要なスタイルのみが生成され、開発時のビルド速度が大幅に向上します。
javascript// vite.config.ts
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
export default defineConfig({
plugins: [solid()],
css: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
// ビルド最適化
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['solid-js'],
},
},
},
},
});
バンドルサイズの最小化
SolidJS と Tailwind CSS の組み合わせでは、以下のようなバンドル分析結果が期待できます:
bash# バンドル分析の実行
yarn add -D vite-bundle-analyzer
# package.jsonに追加
{
"scripts": {
"analyze": "yarn build && npx vite-bundle-analyzer dist"
}
}
典型的な最適化後のバンドルサイズ:
ファイル | サイズ | 圧縮後 |
---|---|---|
main.js | 25KB | 8KB |
main.css | 8KB | 2KB |
vendor.js | 15KB | 5KB |
さらなる最適化のために、以下の設定を追加できます:
typescript// src/index.tsx
import { render } from 'solid-js/web';
import { lazy } from 'solid-js';
// コンポーネントの遅延読み込み
const App = lazy(() => import('./App'));
render(
() => <App />,
document.getElementById('root') as HTMLElement
);
typescript// 条件付きインポートによる軽量化
import type { Component } from 'solid-js';
// 本番環境では開発ツールを除外
const DevTools = import.meta.env.DEV
? lazy(() => import('./DevTools'))
: ((() => null) as Component);
export const App = () => {
return (
<div>
<MainContent />
<DevTools />
</div>
);
};
実用的な UI パターン集
ダッシュボード画面
現代的なダッシュボード UI を実装してみましょう。データの可視化とユーザビリティを両立させた設計になっています。
typescriptimport { createSignal, For } from 'solid-js';
interface MetricCardProps {
title: string;
value: string;
change: number;
icon: string;
}
const MetricCard = (props: MetricCardProps) => {
const isPositive = () => props.change > 0;
return (
<Card class="relative overflow-hidden">
{/* アイコン背景 */}
<div class="absolute top-0 right-0 -mt-4 -mr-4">
<div class="w-24 h-24 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full opacity-10 transform rotate-12"></div>
</div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-medium text-gray-500">{props.title}</h3>
<span class="text-2xl">{props.icon}</span>
</div>
<div class="flex items-end justify-between">
<p class="text-3xl font-bold text-gray-900">{props.value}</p>
<div class={`flex items-center text-sm font-medium ${
isPositive() ? 'text-green-600' : 'text-red-600'
}`}>
<svg
class={`w-4 h-4 mr-1 ${isPositive() ? 'rotate-0' : 'rotate-180'}`}
fill="currentColor"
viewBox="0 0 20 20"
>
<path fill-rule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
{Math.abs(props.change)}%
</div>
</div>
</div>
</Card>
);
};
export const Dashboard = () => {
const [timeRange, setTimeRange] = createSignal('7d');
const metrics = [
{ title: 'アクティブユーザー', value: '2,543', change: 12.5, icon: '👥' },
{ title: '売上', value: '¥1,234,567', change: 8.2, icon: '💰' },
{ title: 'コンバージョン率', value: '3.24%', change: -2.1, icon: '📊' },
{ title: '顧客満足度', value: '4.8', change: 5.3, icon: '⭐' },
];
return (
<div class="space-y-6">
{/* ヘッダー */}
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">ダッシュボード</h1>
<p class="text-gray-500">ビジネスの状況を一目で確認できます</p>
</div>
<div class="mt-4 md:mt-0">
<select
value={timeRange()}
onChange={(e) => setTimeRange(e.currentTarget.value)}
class="px-4 py-2 border border-gray-300 rounded-md bg-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="24h">過去24時間</option>
<option value="7d">過去7日間</option>
<option value="30d">過去30日間</option>
<option value="90d">過去90日間</option>
</select>
</div>
</div>
{/* メトリクスカード */}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<For each={metrics}>
{(metric) => <MetricCard {...metric} />}
</For>
</div>
typescript {/* チャートエリア */}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* メインチャート */}
<Card class="lg:col-span-2">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-gray-900">売上推移</h3>
<div class="flex space-x-2">
<button class="px-3 py-1 text-xs font-medium text-blue-600 bg-blue-50 rounded-full">
売上
</button>
<button class="px-3 py-1 text-xs font-medium text-gray-500 hover:text-gray-700 transition-colors">
利益
</button>
</div>
</div>
{/* シンプルなチャートプレースホルダー */}
<div class="h-64 bg-gradient-to-t from-blue-50 to-transparent rounded-lg relative overflow-hidden">
<svg class="absolute inset-0 w-full h-full" viewBox="0 0 400 200">
<polyline
fill="none"
stroke="#3B82F6"
stroke-width="3"
points="0,150 50,140 100,120 150,110 200,100 250,90 300,85 350,80 400,75"
class="drop-shadow-sm"
/>
<circle cx="400" cy="75" r="4" fill="#3B82F6" class="animate-pulse"/>
</svg>
</div>
</Card>
{/* サイドパネル */}
<Card>
<h3 class="text-lg font-semibold text-gray-900 mb-6">今日のハイライト</h3>
<div class="space-y-4">
<div class="flex items-center p-3 bg-green-50 rounded-lg">
<div class="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<div class="flex-1">
<p class="text-sm font-medium text-green-900">新規登録が急増</p>
<p class="text-xs text-green-700">前日比 +45%</p>
</div>
</div>
<div class="flex items-center p-3 bg-blue-50 rounded-lg">
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<div class="flex-1">
<p class="text-sm font-medium text-blue-900">大口契約成立</p>
<p class="text-xs text-blue-700">¥500万の契約</p>
</div>
</div>
<div class="flex items-center p-3 bg-purple-50 rounded-lg">
<div class="w-2 h-2 bg-purple-500 rounded-full mr-3"></div>
<div class="flex-1">
<p class="text-sm font-medium text-purple-900">機能リリース</p>
<p class="text-xs text-purple-700">ユーザー分析機能</p>
</div>
</div>
</div>
</Card>
</div>
</div>
);
};
ランディングページ
コンバージョンを重視したランディングページの UI パターンです。視覚的インパクトと情報の整理を両立させています。
typescriptexport const LandingPage = () => {
const [email, setEmail] = createSignal('');
const handleSubmit = (e: Event) => {
e.preventDefault();
console.log('Email submitted:', email());
};
return (
<div class='min-h-screen'>
{/* ヒーローセクション */}
<section class='relative overflow-hidden bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-800'>
<div class='absolute inset-0'>
<svg
class='absolute bottom-0 left-0 right-0'
viewBox='0 0 1440 120'
fill='none'
>
<path
d='M0,64L1440,96L1440,120L0,120Z'
fill='white'
/>
</svg>
</div>
<div class='relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 lg:py-32'>
<div class='text-center'>
<h1 class='text-4xl md:text-6xl font-bold text-white mb-6 leading-tight'>
美しいUIを
<br class='hidden md:block' />
<span class='text-yellow-300'>最速で</span>
作ろう
</h1>
<p class='text-xl md:text-2xl text-blue-100 mb-8 max-w-3xl mx-auto'>
SolidJS × Tailwind
CSSで実現する、次世代のフロントエンド開発体験
</p>
{/* CTAフォーム */}
<form
onSubmit={handleSubmit}
class='max-w-md mx-auto'
>
<div class='flex flex-col sm:flex-row gap-3'>
<input
type='email'
placeholder='メールアドレスを入力'
value={email()}
onInput={(e) =>
setEmail(e.currentTarget.value)
}
class='flex-1 px-4 py-3 rounded-lg border-0 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-4 focus:ring-white/20'
required
/>
<Button
type='submit'
variant='solid'
size='lg'
class='bg-yellow-400 text-gray-900 hover:bg-yellow-300 font-semibold'
>
無料で始める
</Button>
</div>
<p class='text-sm text-blue-200 mt-3'>
7日間無料トライアル・クレジットカード不要
</p>
</form>
</div>
</div>
</section>
{/* 特徴セクション */}
<section class='py-20 bg-white'>
<div class='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
<div class='text-center mb-16'>
<h2 class='text-3xl md:text-4xl font-bold text-gray-900 mb-4'>
なぜ選ばれるのか
</h2>
<p class='text-xl text-gray-600 max-w-2xl mx-auto'>
開発効率と品質を両立する3つの理由
</p>
</div>
<div class='grid grid-cols-1 md:grid-cols-3 gap-12'>
{[
{
icon: '⚡',
title: '超高速開発',
description:
'コンポーネントの作成から本番デプロイまで、従来の半分の時間で完了',
},
{
icon: '🎨',
title: '美しいデザイン',
description:
'デザイナーが作ったモックアップを、コードで完璧に再現',
},
{
icon: '🚀',
title: '高パフォーマンス',
description:
'React比で3倍高速、バンドルサイズは50%削減を実現',
},
].map((feature, index) => (
<div key={index} class='text-center'>
<div class='text-6xl mb-6'>
{feature.icon}
</div>
<h3 class='text-xl font-semibold text-gray-900 mb-4'>
{feature.title}
</h3>
<p class='text-gray-600 leading-relaxed'>
{feature.description}
</p>
</div>
))}
</div>
</div>
</section>
</div>
);
};
管理画面の UI 設計
効率的なデータ管理を可能にする管理画面 UI を実装します。
typescriptimport { createSignal, For } from 'solid-js';
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
status: 'active' | 'inactive';
lastActive: string;
}
export const AdminPanel = () => {
const [users, setUsers] = createSignal<User[]>([
{
id: 1,
name: '田中太郎',
email: 'tanaka@example.com',
role: 'admin',
status: 'active',
lastActive: '2分前',
},
{
id: 2,
name: '佐藤花子',
email: 'sato@example.com',
role: 'user',
status: 'active',
lastActive: '1時間前',
},
{
id: 3,
name: '鈴木一郎',
email: 'suzuki@example.com',
role: 'moderator',
status: 'inactive',
lastActive: '3日前',
},
]);
const [searchTerm, setSearchTerm] = createSignal('');
const [selectedRole, setSelectedRole] =
createSignal('all');
const filteredUsers = () => {
return users().filter((user) => {
const matchesSearch =
user.name
.toLowerCase()
.includes(searchTerm().toLowerCase()) ||
user.email
.toLowerCase()
.includes(searchTerm().toLowerCase());
const matchesRole =
selectedRole() === 'all' ||
user.role === selectedRole();
return matchesSearch && matchesRole;
});
};
const getRoleBadgeClass = (role: string) => {
const classes = {
admin: 'bg-red-100 text-red-800',
moderator: 'bg-yellow-100 text-yellow-800',
user: 'bg-green-100 text-green-800',
};
return `inline-flex px-2 py-1 text-xs font-semibold rounded-full ${classes[role]}`;
};
const getStatusBadgeClass = (status: string) => {
return status === 'active'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800';
};
return (
<div class='space-y-6'>
{/* ヘッダー */}
<div class='flex flex-col md:flex-row md:items-center md:justify-between'>
<div>
<h1 class='text-2xl font-bold text-gray-900'>
ユーザー管理
</h1>
<p class='text-gray-500'>
システムユーザーの管理・編集ができます
</p>
</div>
<div class='mt-4 md:mt-0'>
<Button variant='solid' color='blue'>
新規ユーザー追加
</Button>
</div>
</div>
{/* フィルター */}
<Card>
<div class='grid grid-cols-1 md:grid-cols-3 gap-4'>
<div>
<label class='block text-sm font-medium text-gray-700 mb-2'>
検索
</label>
<input
type='text'
placeholder='名前またはメールで検索'
value={searchTerm()}
onInput={(e) =>
setSearchTerm(e.currentTarget.value)
}
class='w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
/>
</div>
<div>
<label class='block text-sm font-medium text-gray-700 mb-2'>
役割
</label>
<select
value={selectedRole()}
onChange={(e) =>
setSelectedRole(e.currentTarget.value)
}
class='w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
>
<option value='all'>すべて</option>
<option value='admin'>管理者</option>
<option value='moderator'>
モデレーター
</option>
<option value='user'>一般ユーザー</option>
</select>
</div>
<div class='flex items-end'>
<Button
variant='outline'
color='gray'
class='w-full'
>
フィルターをリセット
</Button>
</div>
</div>
</Card>
{/* ユーザーテーブル */}
<Card>
<div class='overflow-x-auto'>
<table class='min-w-full divide-y divide-gray-200'>
<thead class='bg-gray-50'>
<tr>
<th class='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
ユーザー
</th>
<th class='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
役割
</th>
<th class='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
ステータス
</th>
<th class='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
最終アクティブ
</th>
<th class='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
操作
</th>
</tr>
</thead>
<tbody class='bg-white divide-y divide-gray-200'>
<For each={filteredUsers()}>
{(user) => (
<tr class='hover:bg-gray-50 transition-colors'>
<td class='px-6 py-4 whitespace-nowrap'>
<div class='flex items-center'>
<div class='w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold text-sm'>
{user.name.charAt(0)}
</div>
<div class='ml-4'>
<div class='text-sm font-medium text-gray-900'>
{user.name}
</div>
<div class='text-sm text-gray-500'>
{user.email}
</div>
</div>
</div>
</td>
<td class='px-6 py-4 whitespace-nowrap'>
<span
class={getRoleBadgeClass(user.role)}
>
{user.role === 'admin'
? '管理者'
: user.role === 'moderator'
? 'モデレーター'
: '一般ユーザー'}
</span>
</td>
<td class='px-6 py-4 whitespace-nowrap'>
<span
class={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadgeClass(
user.status
)}`}
>
{user.status === 'active'
? 'アクティブ'
: '非アクティブ'}
</span>
</td>
<td class='px-6 py-4 whitespace-nowrap text-sm text-gray-500'>
{user.lastActive}
</td>
<td class='px-6 py-4 whitespace-nowrap text-right text-sm font-medium'>
<div class='flex space-x-2'>
<Button
variant='ghost'
color='blue'
size='sm'
>
編集
</Button>
<Button
variant='ghost'
color='red'
size='sm'
>
削除
</Button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
{filteredUsers().length === 0 && (
<div class='text-center py-12'>
<p class='text-gray-500'>
条件に一致するユーザーが見つかりません
</p>
</div>
)}
</Card>
</div>
);
};
まとめ
SolidJS × Tailwind CSS の組み合わせは、現代のフロントエンド開発において革命的な体験をもたらします。
この記事を通じて、皆様に以下のことをお伝えできたのではないでしょうか:
開発効率の劇的な向上 従来の React ベースの開発と比較して、コンポーネントの作成からスタイリングまでの時間が大幅に短縮されます。特に、SolidJS のリアクティビティと Tailwind CSS のユーティリティクラスの相性は抜群で、思考のスピードで UI を実装していくことが可能になります。
保守性と可読性の両立 型安全性を保ちながら、直感的で分かりやすいコードが書けることは、チーム開発において大きなアドバンテージです。6 ヶ月後に見返しても理解できるコード、新しいメンバーがすぐに理解できるコンポーネント設計は、プロジェクトの長期的な成功に直結します。
パフォーマンスの圧倒的優位性 バンドルサイズの最小化、高速な初期表示、スムーズなユーザーインタラクション。これらすべてが、ユーザー体験の向上につながり、最終的にはビジネスの成功に貢献します。
フロントエンド開発の世界は日々進化していますが、SolidJS × Tailwind CSS の組み合わせは、その中でも特に実用性と将来性を兼ね備えた選択肢だと確信しています。
まずは小さなプロジェクトから始めて、この素晴らしい開発体験を実際に体感してみてください。きっと、従来の開発手法に戻ることは難しくなるはずです。
最後に、技術は使ってこそ価値があります。この記事が、皆様の開発効率向上と、より良いプロダクト作りの一助となれば幸いです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来