T-CREATOR

Emotion で Media Queries を柔軟に扱うテクニック

Emotion で Media Queries を柔軟に扱うテクニック

モダンな Web 開発において、CSS-in-JS は非常に強力な手法として広く採用されています。特に React エコシステムでは、Emotion が多くの開発者に愛用されており、コンポーネント指向のスタイリングを実現できますね。

しかし、Emotion を使ってレスポンシブデザインを実装する際、メディアクエリの扱いで悩んだ経験はありませんか。従来の CSS と異なる記法や、複雑になりがちなブレークポイント管理など、初めて触れる方にとっては少し戸惑うポイントも多いでしょう。

今回は、Emotion でのメディアクエリを効率的かつ柔軟に扱うためのテクニックをご紹介します。基本的な書き方から、実際のプロジェクトで活用できる実践的な手法まで、段階的に学んでいきましょう。

背景

現代の Web 開発では、スマートフォン、タブレット、デスクトップなど、様々なデバイスに対応したレスポンシブデザインが必須となっています。

CSS-in-JS の代表格である Emotion は、JavaScript の中でスタイルを記述できる革新的なライブラリです。コンポーネントと密結合したスタイリングが可能で、TypeScript との相性も抜群ですね。

typescript// Emotion の基本的な使用例
import { css } from '@emotion/react'

const buttonStyle = css`
  padding: 12px 24px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
`

しかし、従来の CSS ファイルと異なり、JavaScript 内でメディアクエリを扱う際には独特の書き方や考え方が必要になります。特に大規模なアプリケーションでは、ブレークポイントの一元管理や、再利用可能なメディアクエリパターンの構築が重要な課題となるでしょう。

課題

Emotion でメディアクエリを実装する際、多くの開発者が直面する課題をいくつかご紹介しましょう。これらの課題を理解することで、より効果的な解決策を見つけることができますね。

記法の違いによる混乱

従来の CSS では、メディアクエリは非常にシンプルに記述できました。

css/* 従来の CSS */
.button {
  padding: 12px 24px;
}

@media (max-width: 768px) {
  .button {
    padding: 8px 16px;
    font-size: 14px;
  }
}

しかし、Emotion では JavaScript のテンプレートリテラル内でメディアクエリを記述するため、初見では戸惑う方も多いのではないでしょうか。

typescript// Emotion での基本的なメディアクエリ
const buttonStyle = css`
  padding: 12px 24px;
  
  @media (max-width: 768px) {
    padding: 8px 16px;
    font-size: 14px;
  }
`

ブレークポイント管理の複雑化

プロジェクトが大きくなるにつれて、ブレークポイントの値がコンポーネント間で重複し、管理が困難になります。以下のような問題が発生しがちですね。

typescript// 問題のあるパターン:ハードコーディングされたブレークポイント
const headerStyle = css`
  @media (max-width: 768px) { /* タブレット用 */ }
`

const sidebarStyle = css`
  @media (max-width: 767px) { /* 微妙に異なる値 */ }
`

const cardStyle = css`
  @media (max-width: 720px) { /* さらに異なる値 */ }
`

このような状況では、デザインの一貫性を保つことが難しく、メンテナンス性も大幅に低下してしまいます。

TypeScript との型安全性の確保

TypeScript を使用している場合、ブレークポイントの値や、メディアクエリ関連の定数に対する型安全性の確保も重要な課題です。

typescript// 型安全性が不十分なパターン
const breakpoint = '768px' // string 型では制約が不十分

const mediaQuery = `@media (max-width: ${breakpoint})`
// このパターンではタイポや無効な値の検出が困難

パフォーマンスの最適化

メディアクエリを多用するコンポーネントでは、不必要な再描画や、CSS の重複が発生する可能性があります。特に動的にスタイルを変更する場合、パフォーマンスへの影響を考慮する必要がありますね。

解決策

これらの課題を解決するために、実践的で効果的なアプローチをご紹介します。段階的に学んでいくことで、Emotion でのメディアクエリ実装がぐっと楽になりますよ。

Emotion でのメディアクエリ基本記法

まずは、Emotion でメディアクエリを記述する基本的な方法から見ていきましょう。

Emotion では、CSS テンプレートリテラル内で通常の CSS と同様にメディアクエリを記述できます。

typescriptimport { css } from '@emotion/react'

// 基本的なメディアクエリの書き方
const responsiveStyle = css`
  font-size: 18px;
  color: #333;
  
  /* タブレット以下 */
  @media (max-width: 768px) {
    font-size: 16px;
    padding: 12px;
  }
  
  /* スマートフォン */
  @media (max-width: 480px) {
    font-size: 14px;
    padding: 8px;
  }
`

Emotion の素晴らしい点は、JavaScript の変数や関数を直接組み込めることです。

typescript// 変数を使ったメディアクエリ
const primaryColor = '#007bff'
const tabletBreakpoint = '768px'

const dynamicStyle = css`
  background-color: ${primaryColor};
  padding: 16px;
  
  @media (max-width: ${tabletBreakpoint}) {
    padding: 12px;
    background-color: ${primaryColor}CC; /* 透明度を追加 */
  }
`

breakpoints の効率的な管理方法

ブレークポイントの一元管理は、大規模プロジェクトでは必須の技術です。以下のような構造で管理することをお勧めします。

typescript// breakpoints.ts - ブレークポイント定義ファイル
export const breakpoints = {
  mobile: '480px',
  tablet: '768px',
  desktop: '1024px',
  large: '1200px',
} as const

// TypeScript の型安全性も確保
export type BreakpointKey = keyof typeof breakpoints

さらに、型安全性を向上させるために、以下のようなヘルパー型も定義できます。

typescript// より厳密な型定義
export type BreakpointValue = typeof breakpoints[BreakpointKey]

// 使用例での型チェック
const getBreakpoint = (key: BreakpointKey): BreakpointValue => {
  return breakpoints[key]
}

このブレークポイント管理により、以下のようなエラーを防ぐことができます。

typescript// ❌ よくあるエラーパターン
// TypeError: Cannot read property 'mobile' of undefined
const badStyle = css`
  @media (max-width: ${breakpoints.mobil}) { /* タイポエラー */ }
`

// ✅ 型安全な書き方
const goodStyle = css`
  @media (max-width: ${breakpoints.mobile}) {
    font-size: 14px;
  }
`

mq ヘルパー関数の活用

メディアクエリをより簡潔に書くためのヘルパー関数を作成しましょう。この関数により、コードの可読性が大幅に向上します。

typescript// mediaQueries.ts - メディアクエリヘルパー
import { breakpoints } from './breakpoints'

// メディアクエリ生成関数
export const mq = {
  mobile: `@media (max-width: ${breakpoints.mobile})`,
  tablet: `@media (max-width: ${breakpoints.tablet})`,
  desktop: `@media (min-width: ${breakpoints.desktop})`,
  large: `@media (min-width: ${breakpoints.large})`,
  
  // カスタムブレークポイント用
  custom: (width: string) => `@media (max-width: ${width})`,
  
  // min-width と max-width を組み合わせた範囲指定
  between: (min: string, max: string) => 
    `@media (min-width: ${min}) and (max-width: ${max})`,
}

この mq ヘルパーを使用することで、メディアクエリの記述が格段にシンプルになります。

typescriptimport { css } from '@emotion/react'
import { mq } from './mediaQueries'

// ヘルパー関数を使った簡潔な記述
const cardStyle = css`
  padding: 24px;
  margin: 16px;
  background: white;
  border-radius: 8px;
  
  ${mq.tablet} {
    padding: 16px;
    margin: 12px;
  }
  
  ${mq.mobile} {
    padding: 12px;
    margin: 8px;
    border-radius: 4px;
  }
`

具体例

理論だけでなく、実際のプロジェクトで使える具体的な実装例をご紹介しましょう。これらのパターンを理解することで、実務でも自信を持って対応できるようになりますね。

基本的なメディアクエリの書き方

実際のコンポーネントで、レスポンシブデザインを実装してみましょう。

typescriptimport React from 'react'
import { css } from '@emotion/react'
import { mq } from '../utils/mediaQueries'

// プロダクトカードコンポーネント
interface ProductCardProps {
  title: string
  price: number
  imageUrl: string
  description: string
}

const ProductCard: React.FC<ProductCardProps> = ({ 
  title, price, imageUrl, description 
}) => {
  return (
    <div css={productCardStyle}>
      <img src={imageUrl} alt={title} css={imageStyle} />
      <div css={contentStyle}>
        <h3 css={titleStyle}>{title}</h3>
        <p css={descriptionStyle}>{description}</p>
        <span css={priceStyle}>¥{price.toLocaleString()}</span>
      </div>
    </div>
  )
}

スタイル定義では、デバイスサイズごとに最適化された表示を実現します。

typescriptconst productCardStyle = css`
  display: flex;
  flex-direction: row;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  margin-bottom: 24px;
  
  ${mq.tablet} {
    flex-direction: column;
    margin-bottom: 16px;
  }
  
  ${mq.mobile} {
    border-radius: 8px;
    margin-bottom: 12px;
  }
`

const imageStyle = css`
  width: 200px;
  height: 150px;
  object-fit: cover;
  
  ${mq.tablet} {
    width: 100%;
    height: 200px;
  }
  
  ${mq.mobile} {
    height: 160px;
  }
`

カスタムフックを使った responsive 対応

React のカスタムフックを使用して、現在の画面サイズを監視し、動的にスタイルを変更する方法をご紹介します。

typescript// hooks/useMediaQuery.ts
import { useState, useEffect } from 'react'
import { breakpoints } from '../utils/breakpoints'

export const useMediaQuery = (query: string): boolean => {
  const [matches, setMatches] = useState(false)

  useEffect(() => {
    const mediaQuery = window.matchMedia(query)
    setMatches(mediaQuery.matches)

    const handleChange = (event: MediaQueryListEvent) => {
      setMatches(event.matches)
    }

    mediaQuery.addEventListener('change', handleChange)
    
    return () => {
      mediaQuery.removeEventListener('change', handleChange)
    }
  }, [query])

  return matches
}

// 便利なブレークポイント判定フック
export const useBreakpoint = () => {
  const isMobile = useMediaQuery(`(max-width: ${breakpoints.mobile})`)
  const isTablet = useMediaQuery(`(max-width: ${breakpoints.tablet})`)
  const isDesktop = useMediaQuery(`(min-width: ${breakpoints.desktop})`)
  
  return { isMobile, isTablet, isDesktop }
}

このカスタムフックを使用することで、JavaScript レベルでのレスポンシブ対応が可能になります。

typescript// コンポーネントでの使用例
import React from 'react'
import { css } from '@emotion/react'
import { useBreakpoint } from '../hooks/useMediaQuery'

const ResponsiveNavigation: React.FC = () => {
  const { isMobile, isTablet } = useBreakpoint()
  
  // モバイルでは異なるナビゲーション構造を表示
  if (isMobile) {
    return <MobileNavigation />
  }
  
  // タブレットでは一部機能を簡素化
  if (isTablet) {
    return <TabletNavigation />
  }
  
  // デスクトップでは全機能を表示
  return <DesktopNavigation />
}

実用的な例として、画像ギャラリーのグリッドレイアウトも実装してみましょう。

typescriptconst ImageGallery: React.FC<{ images: string[] }> = ({ images }) => {
  const { isMobile, isTablet } = useBreakpoint()
  
  // デバイスに応じてグリッドの列数を動的に決定
  const getGridColumns = () => {
    if (isMobile) return 1
    if (isTablet) return 2
    return 3
  }
  
  const galleryStyle = css`
    display: grid;
    grid-template-columns: repeat(${getGridColumns()}, 1fr);
    gap: 16px;
    padding: 16px;
  `
  
  return (
    <div css={galleryStyle}>
      {images.map((src, index) => (
        <img key={index} src={src} css={imageGridStyle} />
      ))}
    </div>
  )
}

テーマプロバイダーとの連携

最後に、Emotion の Theme Provider と組み合わせて、より柔軟なメディアクエリ管理を実現しましょう。

typescript// theme.ts - テーマ定義
import { breakpoints } from './breakpoints'

export interface Theme {
  colors: {
    primary: string
    secondary: string
    background: string
  }
  breakpoints: typeof breakpoints
  spacing: {
    small: string
    medium: string
    large: string
  }
}

export const theme: Theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    background: '#f8f9fa'
  },
  breakpoints,
  spacing: {
    small: '8px',
    medium: '16px',
    large: '24px'
  }
}

テーマを利用したコンポーネントの実装では、型安全性を保ちながら統一されたデザインシステムを構築できます。

typescript// ThemeProvider の設定
import React from 'react'
import { ThemeProvider } from '@emotion/react'
import { theme } from './theme'

const App: React.FC = () => {
  return (
    <ThemeProvider theme={theme}>
      <MainContent />
    </ThemeProvider>
  )
}

テーマを活用したコンポーネントの実装例です。

typescriptimport React from 'react'
import { css, useTheme } from '@emotion/react'
import { Theme } from '../theme'

const ThemedButton: React.FC<{ children: React.ReactNode }> = ({ 
  children 
}) => {
  const theme = useTheme() as Theme
  
  const buttonStyle = css`
    background-color: ${theme.colors.primary};
    color: white;
    padding: ${theme.spacing.medium} ${theme.spacing.large};
    border: none;
    border-radius: 8px;
    font-size: 16px;
    cursor: pointer;
    transition: all 0.2s ease;
    
    @media (max-width: ${theme.breakpoints.tablet}) {
      padding: ${theme.spacing.small} ${theme.spacing.medium};
      font-size: 14px;
    }
    
    @media (max-width: ${theme.breakpoints.mobile}) {
      width: 100%;
      padding: ${theme.spacing.medium};
    }
    
    &:hover {
      background-color: ${theme.colors.secondary};
      transform: translateY(-2px);
    }
  `
  
  return (
    <button css={buttonStyle}>
      {children}
    </button>
  )
}

まとめ

Emotion でのメディアクエリ実装について、基本的な書き方から実践的なテクニックまでご紹介してきました。これらの手法を活用することで、保守性が高く、柔軟なレスポンシブデザインを実現できるでしょう。

重要なポイントの振り返り

#ポイント効果
1ブレークポイントの一元管理デザインの一貫性と保守性の向上
2mq ヘルパー関数の活用コードの可読性と記述効率の向上
3TypeScript との型安全性ランタイムエラーの防止と開発効率の向上
4カスタムフックによる動的対応JavaScript レベルでの柔軟な制御
5Theme Provider との連携統一されたデザインシステムの構築

次のステップに向けて

今回学んだテクニックを実際のプロジェクトで活用する際は、以下の点を意識してください。

パフォーマンスを考慮した実装では、不必要なメディアクエリの重複を避け、CSS の最適化を心がけましょう。特に大規模なアプリケーションでは、バンドルサイズへの影響も考慮することが重要です。

チーム開発での一貫性を保つため、プロジェクト初期段階でブレークポイントやヘルパー関数の定義を決めておくことをお勧めします。これにより、開発者全員が同じ基準でレスポンシブデザインを実装できますね。

アクセシビリティへの配慮も忘れずに。prefers-reduced-motionprefers-color-scheme などのユーザー設定に応じたメディアクエリも積極的に活用していきましょう。

typescript// アクセシビリティを考慮したメディアクエリ例
const accessibleAnimationStyle = css`
  transition: all 0.3s ease;
  
  @media (prefers-reduced-motion: reduce) {
    transition: none;
  }
  
  @media (prefers-color-scheme: dark) {
    background-color: #333;
    color: #fff;
  }
`

CSS-in-JS の世界は常に進化しています。Emotion での メディアクエリ実装をマスターすることで、より高品質で使いやすい Web アプリケーションを作り上げていけるでしょう。

関連リンク