Emotion で SVG アイコンや画像にスタイルを適用する

現代の Web 開発において、SVG アイコンや画像のスタイリングは重要な要素です。特に React アプリケーションでは、動的なスタイリングや状態に応じた見た目の変更が求められることが多くなりました。
CSS-in-JS ライブラリの Emotion を使用することで、これらの課題を効率的に解決できます。従来の CSS ファイルでは実現が困難だった柔軟なスタイリングを、JavaScript の力を借りて実現していきましょう。
背景
モダン Web 開発における SVG・画像スタイリングの重要性
現在の Web アプリケーション開発では、ユーザーエクスペリエンスの向上のため、視覚的な要素が重要な役割を果たしています。特に以下の要求が高まっています。
- 動的テーマ切り替え: ダークモード・ライトモードの対応
- インタラクティブ性: ホバーやクリック時の視覚的フィードバック
- レスポンシブデザイン: デバイスサイズに応じた最適化
- パフォーマンス最適化: 画像の遅延読み込みやサイズ調整
以下の図で、現代的な Web アプリケーションにおける視覚要素の関係性を示します。
mermaidflowchart TD
user[ユーザー操作] -->|テーマ切り替え| theme[テーマ状態]
user -->|デバイス変更| device[デバイス状態]
user -->|インタラクション| interaction[インタラクション状態]
theme --> styles[動的スタイル]
device --> styles
interaction --> styles
styles --> svg[SVG アイコン]
styles --> image[画像要素]
svg --> ui[UI コンポーネント]
image --> ui
Emotion が提供する価値
Emotion は CSS-in-JS の中でも特に以下の特徴を持っています。
特徴 | 内容 | メリット |
---|---|---|
パフォーマンス | 必要な CSS のみを生成 | バンドルサイズの最適化 |
TypeScript サポート | 型安全なスタイリング | 開発時のエラー防止 |
動的スタイリング | JavaScript での条件分岐 | 柔軟な表現力 |
コンポーネント指向 | React との親和性 | 保守性の向上 |
課題
従来の CSS での SVG・画像スタイリングの限界
従来の CSS ファイルを使ったスタイリングでは、以下のような課題がありました。
mermaidflowchart LR
css[CSS ファイル] -->|静的| fixed[固定スタイル]
fixed --> limit1[テーマ切り替え困難]
fixed --> limit2[動的変更不可]
fixed --> limit3[状態管理複雑]
limit1 --> problem[開発・保守の困難]
limit2 --> problem
limit3 --> problem
主な課題点
1. 静的なスタイル定義
- CSS は基本的に静的なルールセットのため、JavaScript の状態に応じた動的な変更が困難
- メディアクエリだけでは対応できない複雑な条件分岐
2. グローバルスコープ問題
- CSS クラス名の衝突リスク
- スタイルの影響範囲が予測困難
3. 保守性の問題
- CSS と JavaScript の分離による保守の困難さ
- 削除されたコンポーネントに対応する CSS の残存
4. パフォーマンス課題
- 使用していない CSS の読み込み
- 動的なクラス名変更によるレンダリング負荷
以下は従来の CSS でテーマ切り替えを実装した際の複雑さを示すコードです。
css/* 従来の CSS - 複雑なテーマ管理 */
.icon {
width: 24px;
height: 24px;
fill: #000000;
}
.icon--dark-theme {
fill: #ffffff;
}
.icon--primary {
fill: #0066cc;
}
.icon--primary.icon--dark-theme {
fill: #4da6ff;
}
/* 状態の組み合わせが増えると管理が困難 */
.icon--hover:hover {
fill: #0052a3;
}
.icon--hover.icon--dark-theme:hover {
fill: #66b3ff;
}
このように、状態の組み合わせが増えるほど CSS クラスが複雑になり、保守性が著しく低下します。
解決策
Emotion による動的スタイリング手法
Emotion を使用することで、これらの課題を以下のアプローチで解決できます。
mermaidflowchart TD
emotion[Emotion] -->|CSS-in-JS| dynamic[動的スタイル]
emotion -->|スコープ化| scoped[局所化スタイル]
emotion -->|TypeScript| typed[型安全性]
dynamic --> solution1[状態連動スタイル]
scoped --> solution2[名前衝突回避]
typed --> solution3[開発効率向上]
solution1 --> benefit[開発・保守性向上]
solution2 --> benefit
solution3 --> benefit
基本的なセットアップ
まず、Emotion の必要なパッケージをインストールします。
bash# Emotion の基本パッケージをインストール
yarn add @emotion/react @emotion/styled
Next.js プロジェクトの場合は、追加で設定が必要です。
typescript// next.config.js - Emotion の設定
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
emotion: true,
},
};
module.exports = nextConfig;
TypeScript を使用している場合は、型定義も追加します。
typescript// types/emotion.d.ts - Emotion の型定義拡張
import '@emotion/react';
declare module '@emotion/react' {
export interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
text: string;
};
breakpoints: {
mobile: string;
tablet: string;
desktop: string;
};
}
}
基本的な使い方
Emotion の css
プロパティを使った基本的なスタイリング方法を確認しましょう。
typescript// 基本的な Emotion の使用方法
import { css } from '@emotion/react';
const iconStyle = css`
width: 24px;
height: 24px;
fill: #0066cc;
transition: fill 0.2s ease;
`;
具体例
SVG アイコンのスタイリング実装
基本的な SVG アイコンコンポーネント
まず、基本的な SVG アイコンコンポーネントを作成します。
typescript// components/Icon/Icon.tsx - 基本的なアイコンコンポーネント
import React from 'react';
import { css } from '@emotion/react';
interface IconProps {
name: string;
size?: number;
color?: string;
}
const iconBaseStyle = css`
display: inline-block;
vertical-align: middle;
fill: currentColor;
transition: all 0.2s ease;
`;
アイコンのサイズと色を動的に制御する関数を定義します。
typescript// components/Icon/Icon.tsx - 動的スタイル関数
const getIconStyle = (size: number, color?: string) => css`
width: ${size}px;
height: ${size}px;
${color && `fill: ${color};`}
`;
完全なアイコンコンポーネントの実装です。
typescript// components/Icon/Icon.tsx - 完全な実装
export const Icon: React.FC<IconProps> = ({
name,
size = 24,
color,
}) => {
return (
<svg
css={[iconBaseStyle, getIconStyle(size, color)]}
aria-hidden='true'
>
<use href={`#icon-${name}`} />
</svg>
);
};
インタラクティブなアイコンボタン
ホバーやアクティブ状態に応じて変化するアイコンボタンを作成します。
typescript// components/IconButton/IconButton.tsx - インタラクティブなボタン
import React from 'react';
import { css, useTheme } from '@emotion/react';
interface IconButtonProps {
icon: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
テーマに応じた動的スタイリングを実装します。
typescript// components/IconButton/IconButton.tsx - テーマ対応スタイル
const getButtonStyle = (
variant: string,
theme: any,
disabled: boolean
) => css`
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px;
border: none;
border-radius: 4px;
background-color: ${getVariantColor(variant, theme)};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
opacity: ${disabled ? 0.5 : 1};
transition: all 0.2s ease;
&:hover:not(:disabled) {
background-color: ${getVariantHoverColor(
variant,
theme
)};
transform: translateY(-1px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
`;
バリアント(種類)に応じた色の制御関数を定義します。
typescript// components/IconButton/IconButton.tsx - 色制御関数
const getVariantColor = (variant: string, theme: any) => {
switch (variant) {
case 'primary':
return theme.colors.primary;
case 'secondary':
return theme.colors.secondary;
case 'danger':
return '#dc3545';
default:
return theme.colors.primary;
}
};
const getVariantHoverColor = (
variant: string,
theme: any
) => {
switch (variant) {
case 'primary':
return '#0052a3';
case 'secondary':
return '#5a6268';
case 'danger':
return '#c82333';
default:
return '#0052a3';
}
};
完全なアイコンボタンコンポーネントの実装です。
typescript// components/IconButton/IconButton.tsx - 完全な実装
export const IconButton: React.FC<IconButtonProps> = ({
icon,
onClick,
variant = 'primary',
disabled = false,
}) => {
const theme = useTheme();
return (
<button
css={getButtonStyle(variant, theme, disabled)}
onClick={onClick}
disabled={disabled}
type='button'
>
<Icon name={icon} size={20} />
</button>
);
};
画像の responsive スタイリング
基本的な responsive 画像コンポーネント
デバイスサイズに応じて最適化される画像コンポーネントを作成します。
typescript// components/ResponsiveImage/ResponsiveImage.tsx - 基本構造
import React from 'react';
import { css } from '@emotion/react';
interface ResponsiveImageProps {
src: string;
alt: string;
aspectRatio?: number;
objectFit?: 'cover' | 'contain' | 'fill';
loading?: 'lazy' | 'eager';
}
responsive な画像のベーススタイルを定義します。
typescript// components/ResponsiveImage/ResponsiveImage.tsx - ベーススタイル
const imageBaseStyle = css`
width: 100%;
height: auto;
display: block;
border-radius: 8px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
`;
アスペクト比を制御する関数を実装します。
typescript// components/ResponsiveImage/ResponsiveImage.tsx - アスペクト比制御
const getAspectRatioStyle = (
aspectRatio?: number,
objectFit?: string
) => {
if (!aspectRatio) return css``;
return css`
aspect-ratio: ${aspectRatio};
object-fit: ${objectFit || 'cover'};
`;
};
レスポンシブ対応のメディアクエリスタイルを定義します。
typescript// components/ResponsiveImage/ResponsiveImage.tsx - メディアクエリ
const responsiveStyle = css`
/* モバイル */
@media (max-width: 768px) {
border-radius: 4px;
&:hover {
transform: none;
box-shadow: none;
}
}
/* タブレット */
@media (min-width: 769px) and (max-width: 1024px) {
border-radius: 6px;
}
/* デスクトップ */
@media (min-width: 1025px) {
border-radius: 8px;
}
`;
完全な responsive 画像コンポーネントの実装です。
typescript// components/ResponsiveImage/ResponsiveImage.tsx - 完全な実装
export const ResponsiveImage: React.FC<
ResponsiveImageProps
> = ({
src,
alt,
aspectRatio,
objectFit = 'cover',
loading = 'lazy',
}) => {
return (
<img
src={src}
alt={alt}
loading={loading}
css={[
imageBaseStyle,
getAspectRatioStyle(aspectRatio, objectFit),
responsiveStyle,
]}
/>
);
};
画像ギャラリーコンポーネント
複数の画像を responsive に表示するギャラリーコンポーネントを作成します。
typescript// components/ImageGallery/ImageGallery.tsx - ギャラリー構造
import React from 'react';
import { css } from '@emotion/react';
import { ResponsiveImage } from '../ResponsiveImage';
interface ImageItem {
id: string;
src: string;
alt: string;
caption?: string;
}
interface ImageGalleryProps {
images: ImageItem[];
columns?: number;
gap?: number;
}
グリッドレイアウトのスタイルを定義します。
typescript// components/ImageGallery/ImageGallery.tsx - グリッドスタイル
const getGalleryStyle = (
columns: number,
gap: number
) => css`
display: grid;
grid-template-columns: repeat(${columns}, 1fr);
gap: ${gap}px;
/* レスポンシブ対応 */
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: ${Math.max(gap - 8, 8)}px;
}
@media (min-width: 769px) and (max-width: 1024px) {
grid-template-columns: repeat(
${Math.min(columns, 2)},
1fr
);
gap: ${Math.max(gap - 4, 12)}px;
}
`;
個別の画像アイテムのスタイルを定義します。
typescript// components/ImageGallery/ImageGallery.tsx - 画像アイテムスタイル
const imageItemStyle = css`
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0)
);
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover::after {
opacity: 1;
}
`;
完全な画像ギャラリーコンポーネントの実装です。
typescript// components/ImageGallery/ImageGallery.tsx - 完全な実装
export const ImageGallery: React.FC<ImageGalleryProps> = ({
images,
columns = 3,
gap = 16,
}) => {
return (
<div css={getGalleryStyle(columns, gap)}>
{images.map((image) => (
<div key={image.id} css={imageItemStyle}>
<ResponsiveImage
src={image.src}
alt={image.alt}
aspectRatio={1}
objectFit='cover'
/>
{image.caption && (
<p css={captionStyle}>{image.caption}</p>
)}
</div>
))}
</div>
);
};
const captionStyle = css`
margin-top: 8px;
font-size: 14px;
color: #666;
text-align: center;
`;
hover・active 状態の動的変更
高度なインタラクション状態管理
複数の状態を組み合わせたインタラクティブなコンポーネントを作成します。
typescript// components/InteractiveCard/InteractiveCard.tsx - 状態管理
import React, { useState } from 'react'
import { css } from '@emotion/react'
interface InteractiveCardProps {
title: string
description: string
imageUrl: string
onAction: () => void
}
export const InteractiveCard: React.FC<InteractiveCardProps> = ({
title,
description,
imageUrl,
onAction
}) => {
const [isHovered, setIsHovered] = useState(false)
const [isActive, setIsActive] = useState(false)
const [isFocused, setIsFocused] = useState(false)
状態に応じた動的スタイルを定義します。
typescript// components/InteractiveCard/InteractiveCard.tsx - 動的スタイル
const cardStyle = css`
position: relative;
padding: 24px;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
/* 状態に応じたスタイル変更 */
${isHovered &&
css`
transform: translateY(-4px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
`}
${isActive &&
css`
transform: translateY(-2px) scale(0.98);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
`}
${isFocused &&
css`
outline: 3px solid #0066cc;
outline-offset: 2px;
`}
`;
画像部分の動的スタイルを定義します。
typescript// components/InteractiveCard/InteractiveCard.tsx - 画像スタイル
const imageStyle = css`
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px;
transition: all 0.3s ease;
${isHovered &&
css`
filter: brightness(1.1) saturate(1.2);
`}
${isActive &&
css`
filter: brightness(0.9) saturate(0.8);
`}
`;
テキスト部分の動的スタイルを定義します。
typescript// components/InteractiveCard/InteractiveCard.tsx - テキストスタイル
const titleStyle = css`
font-size: 18px;
font-weight: 600;
color: #333;
margin: 16px 0 8px;
transition: color 0.2s ease;
${isHovered &&
css`
color: #0066cc;
`}
`;
const descriptionStyle = css`
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 16px;
`;
完全なインタラクティブカードの実装です。
typescript// components/InteractiveCard/InteractiveCard.tsx - 完全な実装
return (
<div
css={cardStyle}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={() => setIsActive(true)}
onMouseUp={() => setIsActive(false)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onClick={onAction}
tabIndex={0}
role="button"
>
<img src={imageUrl} alt={title} css={imageStyle} />
<h3 css={titleStyle}>{title}</h3>
<p css={descriptionStyle}>{description}</p>
</div>
)
}
アニメーション付きボタンコンポーネント
より複雑なアニメーション効果を持つボタンを作成します。
typescript// components/AnimatedButton/AnimatedButton.tsx - アニメーション定義
import React, { useState } from 'react';
import { css, keyframes } from '@emotion/react';
const pulseAnimation = keyframes`
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
`;
const rippleAnimation = keyframes`
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(4);
opacity: 0;
}
`;
ボタンの基本スタイルとアニメーションを定義します。
typescript// components/AnimatedButton/AnimatedButton.tsx - ボタンスタイル
const animatedButtonStyle = css`
position: relative;
padding: 12px 24px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #0066cc, #004c99);
color: white;
font-weight: 600;
cursor: pointer;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #0052a3, #003d7a);
animation: ${pulseAnimation} 0.6s ease-in-out;
}
&:active::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: ${rippleAnimation} 0.4s ease-out;
}
`;
図で理解できる要点:
- Emotion により JavaScript の状態と CSS スタイルが直接連動
- コンポーネント単位でスタイルが局所化されるため保守性が向上
- 動的な条件分岐によりテーマやインタラクション状態を柔軟に制御
まとめ
Emotion を使用した SVG アイコンや画像のスタイリングにより、以下のメリットを得ることができました。
実現できたこと
動的スタイリング
- JavaScript の状態に応じたリアルタイムなスタイル変更
- テーマ切り替えやインタラクション状態の柔軟な制御
保守性の向上
- コンポーネント単位でのスタイル局所化
- TypeScript による型安全なスタイリング
パフォーマンス最適化
- 必要な CSS のみの生成と適用
- 動的なスタイル変更による効率的なレンダリング
開発効率の向上
- CSS-in-JS による開発フローの統一
- 豊富なツールサポートとエラーハンドリング
開発のポイント
Emotion を効果的に活用するには、以下の点を意識することが重要です。
- 段階的な導入: 既存プロジェクトでは部分的に導入し、効果を確認しながら範囲を拡大
- コンポーネント設計: 再利用可能なスタイルコンポーネントの作成
- パフォーマンス監視: 動的スタイルによるレンダリング負荷の監視
- チーム開発: スタイリング規約やベストプラクティスの共有
現代の Web 開発において、Emotion は SVG アイコンや画像のスタイリングを効率的かつ柔軟に行うための強力なツールとなるでしょう。ぜひ実際のプロジェクトで活用してみてください。
関連リンク
- article
Emotion で SVG アイコンや画像にスタイルを適用する
- article
Motion(旧 Framer Motion)Variants 完全攻略:staggerChildren・when で複雑アニメを整理する
- article
Emotion の Babel プラグインで開発体験を向上させる
- article
Emotion と Jest/Testing Library で UI テストを快適に
- article
Emotion の@emotion/cache を活用したパフォーマンス最適化
- article
Emotion でアニメーション遅延・遷移を表現する
- article
Homebrew のキャッシュ管理と最適化術
- article
WordPress のインストール完全手順:レンタルサーバー・Docker・ローカルを徹底比較
- article
gpt-oss で始めるローカル環境 AI 開発入門
- article
GPT-5 で変わる自然言語処理:文章生成・要約・翻訳の精度検証
- article
WebSocket と HTTP/2・HTTP/3 の違いを徹底比較
- article
Emotion で SVG アイコンや画像にスタイルを適用する
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来