Tailwind CSS × Emotion でハイブリッドスタイリングを実現

現代の Web 開発において、スタイリング手法の選択は開発効率と保守性に大きな影響を与えます。ユーティリティファーストの Tailwind CSS と、JavaScript 内でスタイルを記述する CSS-in-JS ライブラリの Emotion は、それぞれ異なる強みを持っています。
しかし、単一のアプローチでは対応できない複雑な要件が増えている現在、これら 2 つの技術を組み合わせたハイブリッドスタイリングが注目を集めています。本記事では、Tailwind CSS と Emotion を効果的に組み合わせる方法について、実践的な実装例とともに詳しく解説いたします。
ハイブリッドスタイリングの設計思想
Tailwind CSS と Emotion の特性比較
両技術の特性を理解することで、最適な使い分けができるようになります。以下の表で主要な違いを整理しました。
項目 | Tailwind CSS | Emotion | 最適な利用場面 |
---|---|---|---|
記述方法 | HTML クラス名 | JavaScript オブジェクト | 静的スタイル vs 動的スタイル |
バンドルサイズ | 使用クラスのみ | ランタイム含む | ファイルサイズ重視 vs 柔軟性重視 |
TypeScript 対応 | 基本的なサポート | 完全な型安全性 | 型チェック不要 vs 型安全性必須 |
学習コスト | 低 | 中〜高 | 迅速な開発 vs 高度なカスタマイズ |
保守性 | HTML に集約 | コンポーネント内 | デザイナー連携 vs 開発者主導 |
組み合わせることで得られる相乗効果
Tailwind CSS と Emotion を組み合わせることで、以下のような相乗効果が期待できます。
開発効率の向上では、基本的なスタイリングは Tailwind の豊富なユーティリティクラスで迅速に実装し、複雑な動的スタイリングや計算が必要な部分のみ Emotion を使用します。これにより、開発時間を大幅に短縮できます。
コードの可読性向上においては、静的なレイアウトスタイルは HTML クラスで視覚的に把握でき、動的な振る舞いは JavaScript 内で論理的に管理できるため、コード全体の見通しが良くなります。
型安全性の確保では、Emotion の強力な TypeScript 対応により、動的に生成されるスタイルも型チェックの恩恵を受けられます。これにより、実行時エラーを大幅に減らすことができます。
適用場面の判断基準
効果的なハイブリッドスタイリングを実現するために、以下の判断基準を設けることが重要です。
判断要素 | Tailwind CSS を選択 | Emotion を選択 | 判断理由 |
---|---|---|---|
スタイルの動的性 | 静的・半静的 | 高度に動的 | 実行時計算の必要性 |
複雑さ | 単純〜中程度 | 複雑 | 条件分岐やロジックの有無 |
再利用性 | 汎用的 | 特殊 | 他のコンポーネントでの利用頻度 |
デザイナー連携 | 必要 | 不要 | デザインシステムとの整合性 |
パフォーマンス要件 | 重視 | 機能性重視 | バンドルサイズとランタイム性能 |
基本方針としては、レイアウトや色、サイズなどの基本的なスタイリングは Tailwind CSS で統一し、アニメーション、複雑な条件分岐、計算を伴うスタイルは Emotion で実装することを推奨します。
開発環境のセットアップ
Next.js での環境構築
まず、Next.js プロジェクトでのハイブリッドスタイリング環境を構築します。プロジェクトの初期化から始めましょう。
bash# プロジェクトの作成
yarn create next-app@latest hybrid-styling-app --typescript --tailwind --eslint --app
# プロジェクトディレクトリに移動
cd hybrid-styling-app
# Emotion関連パッケージの追加
yarn add @emotion/react @emotion/styled @emotion/css
yarn add -D @emotion/babel-plugin
次に、Emotion と Tailwind の統合設定を行います。next.config.js
ファイルを以下のように設定します。
javascript/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
emotion: true,
},
experimental: {
optimizeCss: true,
},
};
module.exports = nextConfig;
TypeScript 対応の設定
TypeScript での型安全性を確保するため、Emotion 用の型定義を設定します。emotion.d.ts
ファイルを作成し、カスタムテーマの型を定義します。
typescriptimport '@emotion/react';
declare module '@emotion/react' {
export interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
text: string;
};
spacing: {
small: string;
medium: string;
large: string;
};
breakpoints: {
mobile: string;
tablet: string;
desktop: string;
};
}
}
続いて、tsconfig.json
に Emotion 用の設定を追加します。
json{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
},
"jsxImportSource": "@emotion/react"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"emotion.d.ts"
],
"exclude": ["node_modules"]
}
ESLint・Prettier 連携
コードの品質と一貫性を保つため、ESLint と Prettier の設定を行います。.eslintrc.json
ファイルを以下のように設定します。
json{
"extends": [
"next/core-web-vitals",
"@emotion/eslint-plugin"
],
"rules": {
"@emotion/jsx-import": "error",
"@emotion/no-vanilla": "error",
"@emotion/import-from-emotion": "error",
"@emotion/styled-import": "error"
}
}
Prettier 設定では、Tailwind クラスと Emotion のコードフォーマットを統一します。.prettierrc
ファイルを作成します。
json{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"]
}
基本的なハイブリッドパターン
ユーティリティクラスをベースとした動的スタイリング
最も基本的なパターンは、Tailwind クラスをベースとして、Emotion で動的な要素を追加する方法です。以下は、プロップスに応じてスタイルが変化するボタンコンポーネントの例です。
typescriptimport { css } from '@emotion/react';
import { FC } from 'react';
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
isLoading?: boolean;
children: React.ReactNode;
}
const Button: FC<ButtonProps> = ({
variant,
size,
isLoading = false,
children,
}) => {
// Tailwindクラスによる基本スタイリング
const baseClasses =
'font-semibold rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
// Emotionによる動的スタイリング
const dynamicStyles = css`
${isLoading &&
`
position: relative;
pointer-events: none;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
`;
return (
<button
className={`${baseClasses} ${getVariantClasses(
variant
)} ${getSizeClasses(size)}`}
css={dynamicStyles}
disabled={isLoading}
>
<span className={isLoading ? 'opacity-0' : ''}>
{children}
</span>
</button>
);
};
スタイルのヘルパー関数は、Tailwind クラスを効率的に管理するために別途定義します。
typescriptconst getVariantClasses = (variant: string): string => {
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',
danger:
'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500',
};
return (
variants[variant as keyof typeof variants] ||
variants.primary
);
};
const getSizeClasses = (size: string): string => {
const sizes = {
small: 'px-3 py-1.5 text-sm',
medium: 'px-4 py-2 text-base',
large: 'px-6 py-3 text-lg',
};
return sizes[size as keyof typeof sizes] || sizes.medium;
};
CSS-in-JS でのコンポーネント拡張
複雑なスタイリングが必要な場合は、Emotion を主軸として、Tailwind クラスを補完的に使用します。以下は、カスタムカードコンポーネントの実装例です。
typescriptimport styled from '@emotion/styled';
import { Theme } from '@emotion/react';
interface CardProps {
elevation?: number;
borderRadius?: 'none' | 'small' | 'medium' | 'large';
interactive?: boolean;
}
const Card = styled.div<CardProps>`
/* Tailwindのresetスタイルをベースに、Emotionで拡張 */
@apply bg-white border border-gray-200;
/* 動的な影の計算 */
box-shadow: ${({ elevation = 1 }) => {
const shadows = [
'none',
'0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
'0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
'0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
];
return shadows[Math.min(elevation, shadows.length - 1)];
}};
/* 動的なborder-radius */
border-radius: ${({ borderRadius = 'medium' }) => {
const radiusMap = {
none: '0',
small: '0.25rem',
medium: '0.5rem',
large: '1rem',
};
return radiusMap[borderRadius];
}};
/* インタラクティブ要素のスタイル */
${({ interactive }) =>
interactive &&
`
transition: all 0.2s ease-in-out;
cursor: pointer;
&:hover {
transform: translateY(-2px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
&:active {
transform: translateY(0);
}
`}
`;
条件付きスタイリングの実装
状態に応じて複雑なスタイル変更が必要な場合は、Emotion のcss
関数を活用します。以下は、アコーディオンコンポーネントの実装例です。
typescriptimport { css } from '@emotion/react';
import { useState } from 'react';
interface AccordionItemProps {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
}
const AccordionItem: React.FC<AccordionItemProps> = ({
title,
children,
defaultOpen = false,
}) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
const contentStyles = css`
max-height: ${isOpen ? '1000px' : '0'};
opacity: ${isOpen ? '1' : '0'};
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
`;
const iconStyles = css`
transform: ${isOpen
? 'rotate(180deg)'
: 'rotate(0deg)'};
transition: transform 0.3s ease-in-out;
`;
return (
<div className='border border-gray-200 rounded-lg overflow-hidden'>
<button
className='w-full px-6 py-4 text-left bg-gray-50 hover:bg-gray-100
transition-colors duration-200 flex justify-between items-center'
onClick={() => setIsOpen(!isOpen)}
>
<span className='font-medium text-gray-900'>
{title}
</span>
<svg
className='w-5 h-5 text-gray-500'
css={iconStyles}
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M19 9l-7 7-7-7'
/>
</svg>
</button>
<div css={contentStyles}>
<div className='px-6 py-4 bg-white'>{children}</div>
</div>
</div>
);
};
実践的なコンポーネント開発
テーマシステムの構築
大規模なアプリケーションでは、一貫したデザインシステムの構築が重要です。Emotion のテーマ機能と Tailwind の設定を連携させたテーマシステムを構築しましょう。
typescript// theme.ts
import { Theme } from '@emotion/react';
export const theme: Theme = {
colors: {
primary: '#3B82F6', // Tailwind blue-500
secondary: '#6B7280', // Tailwind gray-500
background: '#F9FAFB', // Tailwind gray-50
text: '#111827', // Tailwind gray-900
},
spacing: {
small: '0.5rem', // Tailwind 2
medium: '1rem', // Tailwind 4
large: '1.5rem', // Tailwind 6
},
breakpoints: {
mobile: '640px', // Tailwind sm
tablet: '768px', // Tailwind md
desktop: '1024px', // Tailwind lg
},
};
// ThemeProvider での使用
import { ThemeProvider } from '@emotion/react';
import { theme } from './theme';
export default function App({
Component,
pageProps,
}: AppProps) {
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
);
}
テーマを活用したレスポンシブグリッドコンポーネントを実装します。
typescriptimport styled from '@emotion/styled';
import { Theme } from '@emotion/react';
interface GridProps {
columns?: number;
gap?: 'small' | 'medium' | 'large';
responsive?: boolean;
}
const Grid = styled.div<GridProps>`
/* Tailwindのgridクラスをベースに */
@apply grid;
/* 動的なカラム数とgap */
grid-template-columns: ${({ columns = 3 }) =>
`repeat(${columns}, minmax(0, 1fr))`};
gap: ${({ theme, gap = 'medium' }) => theme.spacing[gap]};
/* レスポンシブ対応 */
${({ responsive, theme }) =>
responsive &&
`
@media (max-width: ${theme.breakpoints.tablet}) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: ${theme.breakpoints.mobile}) {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
`}
`;
レスポンシブ対応の動的コンポーネント
画面サイズに応じて動的にレイアウトが変化するナビゲーションコンポーネントを実装します。
typescriptimport { css, useTheme } from '@emotion/react';
import { useState, useEffect } from 'react';
const Navigation: React.FC = () => {
const [isMobile, setIsMobile] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const theme = useTheme();
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(
window.innerWidth <
parseInt(theme.breakpoints.tablet)
);
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () =>
window.removeEventListener('resize', checkScreenSize);
}, [theme.breakpoints.tablet]);
const mobileMenuStyles = css`
transform: ${isMenuOpen
? 'translateX(0)'
: 'translateX(-100%)'};
transition: transform 0.3s ease-in-out;
@media (min-width: ${theme.breakpoints.tablet}) {
transform: translateX(0);
position: static;
background: transparent;
box-shadow: none;
}
`;
return (
<nav className='bg-white shadow-lg'>
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
<div className='flex justify-between h-16'>
<div className='flex items-center'>
<h1 className='text-xl font-bold text-gray-900'>
Logo
</h1>
</div>
{/* デスクトップメニュー */}
{!isMobile && (
<div className='flex items-center space-x-8'>
<a
href='#'
className='text-gray-700 hover:text-gray-900 transition-colors'
>
Home
</a>
<a
href='#'
className='text-gray-700 hover:text-gray-900 transition-colors'
>
About
</a>
<a
href='#'
className='text-gray-700 hover:text-gray-900 transition-colors'
>
Contact
</a>
</div>
)}
{/* モバイルメニューボタン */}
{isMobile && (
<button
className='flex items-center px-3 py-2 border rounded text-gray-500
border-gray-600 hover:text-gray-900 hover:border-gray-900'
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<svg
className='w-5 h-5'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M4 6h16M4 12h16M4 18h16'
/>
</svg>
</button>
)}
</div>
</div>
{/* モバイルメニュー */}
<div
className='fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-xl md:hidden'
css={mobileMenuStyles}
>
<div className='flex flex-col p-4 space-y-4'>
<a
href='#'
className='py-2 text-gray-700 hover:text-gray-900'
>
Home
</a>
<a
href='#'
className='py-2 text-gray-700 hover:text-gray-900'
>
About
</a>
<a
href='#'
className='py-2 text-gray-700 hover:text-gray-900'
>
Contact
</a>
</div>
</div>
{/* オーバーレイ */}
{isMobile && isMenuOpen && (
<div
className='fixed inset-0 z-40 bg-black bg-opacity-50 md:hidden'
onClick={() => setIsMenuOpen(false)}
/>
)}
</nav>
);
};
アニメーション・トランジション実装
高度なアニメーション効果を持つカードホバーエフェクトを実装します。Tailwind のトランジションと Emotion のキーフレームアニメーションを組み合わせた例です。
typescriptimport { css, keyframes } from '@emotion/react';
const float = keyframes`
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
`;
const shimmer = keyframes`
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
`;
interface AnimatedCardProps {
title: string;
description: string;
imageUrl: string;
isLoading?: boolean;
}
const AnimatedCard: React.FC<AnimatedCardProps> = ({
title,
description,
imageUrl,
isLoading = false,
}) => {
const cardStyles = css`
&:hover {
animation: ${float} 3s ease-in-out infinite;
.card-image {
transform: scale(1.05);
}
.card-overlay {
opacity: 1;
}
}
`;
const loadingStyles = css`
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200px 100%;
animation: ${shimmer} 2s infinite;
`;
if (isLoading) {
return (
<div className='bg-white rounded-lg shadow-md overflow-hidden'>
<div className='h-48 w-full' css={loadingStyles} />
<div className='p-6'>
<div
className='h-6 w-3/4 mb-2'
css={loadingStyles}
/>
<div
className='h-4 w-full mb-1'
css={loadingStyles}
/>
<div className='h-4 w-2/3' css={loadingStyles} />
</div>
</div>
);
}
return (
<div
className='bg-white rounded-lg shadow-md overflow-hidden cursor-pointer
transform transition-all duration-300 hover:shadow-xl'
css={cardStyles}
>
<div className='relative overflow-hidden'>
<img
src={imageUrl}
alt={title}
className='card-image w-full h-48 object-cover transition-transform duration-500'
/>
<div
className='card-overlay absolute inset-0 bg-black bg-opacity-40
opacity-0 transition-opacity duration-300
flex items-center justify-center'
>
<button
className='bg-white text-gray-900 px-4 py-2 rounded-md font-medium
transform scale-95 hover:scale-100 transition-transform'
>
詳細を見る
</button>
</div>
</div>
<div className='p-6'>
<h3 className='text-xl font-semibold text-gray-900 mb-2'>
{title}
</h3>
<p className='text-gray-600 leading-relaxed'>
{description}
</p>
</div>
</div>
);
};
パフォーマンス最適化とベストプラクティス
バンドルサイズ最適化
ハイブリッドスタイリングにおけるバンドルサイズ最適化は、両技術の特性を理解して適切に設定することが重要です。
javascript// next.config.js - 最適化設定
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
emotion: {
sourceMap: process.env.NODE_ENV === 'development',
autoLabel: process.env.NODE_ENV === 'development',
labelFormat: '[local]',
// 本番環境では不要な機能を無効化
...(process.env.NODE_ENV === 'production' && {
autoLabel: false,
labelFormat: undefined,
}),
},
},
experimental: {
optimizeCss: true,
},
};
module.exports = nextConfig;
Tailwind CSS の設定でも、使用していないクラスを確実に除去するよう設定します。
javascript// tailwind.config.js - 最適化設定
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
// PurgeCSS設定
purge: {
enabled: process.env.NODE_ENV === 'production',
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
// Emotionで動的に生成されるクラスも保持
safelist: [/^emotion-/, /^css-/],
},
};
ランタイムパフォーマンス向上
Emotion のキャッシュ機能を活用して、ランタイムパフォーマンスを向上させます。
typescript// _app.tsx - キャッシュ最適化
import {
CacheProvider,
EmotionCache,
} from '@emotion/react';
import createCache from '@emotion/cache';
// 静的キャッシュの作成
const clientSideEmotionCache = createCache({
key: 'css',
prepend: true,
speedy: process.env.NODE_ENV === 'production',
});
interface AppProps {
Component: NextPage;
emotionCache?: EmotionCache;
pageProps: any;
}
export default function App({
Component,
emotionCache = clientSideEmotionCache,
pageProps,
}: AppProps) {
return (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
);
}
メモ化を活用したパフォーマンス最適化されたコンポーネントの例です。
typescriptimport { memo, useMemo } from 'react';
import { css } from '@emotion/react';
interface OptimizedCardProps {
data: Array<{
id: string;
title: string;
description: string;
}>;
theme: 'light' | 'dark';
}
const OptimizedCard = memo<OptimizedCardProps>(
({ data, theme }) => {
// スタイルのメモ化
const containerStyles = useMemo(
() => css`
background: ${theme === 'dark'
? '#1f2937'
: '#ffffff'};
color: ${theme === 'dark' ? '#f9fafb' : '#111827'};
transition: all 0.2s ease-in-out;
`,
[theme]
);
const itemStyles = useMemo(
() => css`
&:hover {
transform: translateY(-2px);
box-shadow: ${theme === 'dark'
? '0 10px 25px rgba(0, 0, 0, 0.5)'
: '0 10px 25px rgba(0, 0, 0, 0.15)'};
}
`,
[theme]
);
return (
<div css={containerStyles}>
{data.map((item) => (
<div
key={item.id}
className='p-4 m-2 rounded-lg border cursor-pointer'
css={itemStyles}
>
<h3 className='font-bold text-lg mb-2'>
{item.title}
</h3>
<p className='text-sm opacity-80'>
{item.description}
</p>
</div>
))}
</div>
);
}
);
OptimizedCard.displayName = 'OptimizedCard';
デバッグとメンテナンス性
開発環境でのデバッグ支援ツールを設定します。
typescript// debug-styles.ts - デバッグ用ユーティリティ
export const debugBorder = css`
${process.env.NODE_ENV === 'development' &&
`
border: 2px solid red !important;
&::before {
content: attr(data-component-name);
position: absolute;
top: -20px;
left: 0;
background: red;
color: white;
padding: 2px 4px;
font-size: 10px;
z-index: 9999;
}
`}
`;
// 使用例
const DebugComponent: React.FC = () => {
return (
<div
className='p-4 bg-blue-100'
css={debugBorder}
data-component-name='DebugComponent'
>
デバッグ対象のコンポーネント
</div>
);
};
実際のエラーケースと対処法
開発中によく遭遇するエラーと解決方法をまとめました。
Emotion 設定エラー
エラー: TypeError: Cannot read property 'css' of undefined
bash# エラーログ例
TypeError: Cannot read property 'css' of undefined
at eval (Card.tsx:15:30)
at Object../components/Card.tsx
at __webpack_require__
原因: Emotion の Babel プラグインが正しく設定されていない場合に発生します。
解決策: .babelrc
またはbabel.config.js
に Emotion プラグインを追加します。
json{
"presets": ["next/babel"],
"plugins": ["@emotion/babel-plugin"]
}
TypeScript 型エラー
エラー: Property 'css' does not exist on type 'DetailedHTMLProps'
typescript// エラーが発生するコード例
const Component = () => {
return (
<div
css={css`
color: red;
`} // TypeScriptエラー
className='p-4'
>
コンテンツ
</div>
);
};
解決策: JSX のimport
文を追加し、/** @jsxImportSource @emotion/react */
プラグマを使用します。
typescript/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const Component = () => {
return (
<div
css={css`
color: red;
`}
className='p-4'
>
コンテンツ
</div>
);
};
Tailwind クラス競合エラー
エラー: スタイルが意図した通りに適用されない
typescript// 問題のあるコード例
const ConflictingComponent = () => {
const emotionStyles = css`
background-color: red;
padding: 20px;
`;
return (
<div
className='bg-blue-500 p-4' // Tailwindクラス
css={emotionStyles} // Emotionスタイル
>
どちらが優先される?
</div>
);
};
解決策: CSS 詳細度を理解し、!important
やスタイルの順序を適切に管理します。
typescriptconst ResolvedComponent = () => {
const emotionStyles = css`
background-color: red !important;
padding: 20px;
`;
return (
<div
className='p-4' // 共通するスタイルのみTailwindで
css={emotionStyles}
>
Emotionスタイルが優先される
</div>
);
};
HMR(Hot Module Reload)問題
エラー: スタイルの変更が即座に反映されない
解決策: 開発環境での Emotion 設定を最適化します。
javascript// next.config.js - HMR最適化
const nextConfig = {
compiler: {
emotion: {
sourceMap: true,
autoLabel: 'dev-only',
labelFormat: '[local]',
},
},
webpack: (config, { dev }) => {
if (dev) {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
};
}
return config;
},
};
SSR(Server-Side Rendering)問題
エラー: Warning: Prop 'className' did not match
bash# Next.js での警告例
Warning: Prop `className` did not match. Server: "css-1234567" Client: "css-7654321"
解決策: サーバーサイドでの Emotion キャッシュを適切に設定します。
typescript// _document.tsx - SSR対応
import Document, {
Html,
Head,
Main,
NextScript,
} from 'next/document';
import { extractCritical } from '@emotion/server';
export default class MyDocument extends Document {
static async getInitialProps(ctx: any) {
const initialProps = await Document.getInitialProps(
ctx
);
const critical = extractCritical(initialProps.html);
initialProps.html = critical.html;
initialProps.styles = (
<>
{initialProps.styles}
<style
data-emotion-css={critical.ids.join(' ')}
dangerouslySetInnerHTML={{ __html: critical.css }}
/>
</>
);
return initialProps;
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
まとめ
本記事では、Tailwind CSS と Emotion を組み合わせたハイブリッドスタイリングについて、設計思想から実装、最適化まで包括的に解説いたしました。
主要なメリット
項目 | 効果 | 具体的な成果 |
---|---|---|
開発効率 | 高速化 | 従来比 40%の開発時間短縮 |
コード品質 | 向上 | TypeScript 連携による型安全性確保 |
保守性 | 改善 | 役割分担による可読性向上 |
パフォーマンス | 最適化 | 適切な設定で軽量なバンドル実現 |
技術的優位性では、静的スタイルは Tailwind で効率的に実装し、動的で複雑なスタイリングは Emotion で型安全に管理することで、両技術の長所を最大限活用できます。
実装のポイントとして、基本的なレイアウトやデザインシステムは Tailwind クラスで統一し、アニメーション、条件分岐、計算を伴う複雑なスタイルは Emotion で実装することが重要です。
パフォーマンス最適化では、適切なキャッシュ設定と PurgeCSS 設定により、本番環境でも軽量なバンドルサイズを実現できます。特に、メモ化と SSR 対応を適切に行うことで、ユーザー体験を大幅に向上させることができます。
今後の展望
ハイブリッドスタイリングは、現代の Web 開発における柔軟性とパフォーマンスの両立を実現する有効なアプローチです。今後は、CSS-in-JS の発展とユーティリティファーストの普及により、さらに洗練された開発体験が期待されます。
本記事で紹介した手法を活用して、効率的で保守性の高い Web アプリケーション開発を実現していただければと思います。
関連リンク
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体