T-CREATOR

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

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

現代の Web 開発において、スタイリング手法の選択は開発効率と保守性に大きな影響を与えます。ユーティリティファーストの Tailwind CSS と、JavaScript 内でスタイルを記述する CSS-in-JS ライブラリの Emotion は、それぞれ異なる強みを持っています。

しかし、単一のアプローチでは対応できない複雑な要件が増えている現在、これら 2 つの技術を組み合わせたハイブリッドスタイリングが注目を集めています。本記事では、Tailwind CSS と Emotion を効果的に組み合わせる方法について、実践的な実装例とともに詳しく解説いたします。

ハイブリッドスタイリングの設計思想

Tailwind CSS と Emotion の特性比較

両技術の特性を理解することで、最適な使い分けができるようになります。以下の表で主要な違いを整理しました。

項目Tailwind CSSEmotion最適な利用場面
記述方法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 アプリケーション開発を実現していただければと思います。

関連リンク