ありがとうstyled-components : CSS Modules移行テクニック
はじめに
2025年3月にstyled-componentsがメンテナンスモードになりました。
新しい技術がどんどん増えていく一方、こういった今まで利用してきたものがメンテナンスモードになっていくのは少し寂しい気持ちになります。
そこで本記事では、styled-componentsからCSS Modulesに移行する方法をいくつか実際のコンポーネント例を用いて紹介していきます。
1. 基本的なスタイル
styled-components
import styled from 'styled-components';
const StyledButton = styled.button`
background-color: #1e88e5;
color: white;
padding: 10px 15px;
border-radius: 4px;
border: none;
cursor: pointer;
`;
const App = () => (
<StyledButton>クリック!</StyledButton>
);
CSS Modules
.button {
background-color: #1e88e5;
color: white;
padding: 10px 15px;
border-radius: 4px;
border: none;
cursor: pointer;
}
import styles from './Button.module.css';
const App = () => (
<button className={styles.button} {...props}>クリック!</button>
);
シンプルな実装であれば移行は比較的容易です。次に動的なスタイル定義を見ていきましょう。
2. propsを利用した動的スタイル
styled-components
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 10px 15px;
border-radius: 4px;
background-color: ${props => props.variant === 'primary' ? '#007bff' :
props.variant === 'danger' ? '#dc3545' : '#f8f9fa'};
color: ${props => props.variant === 'primary' ||
props.variant === 'danger' ? '#ffffff' : '#212529'};
font-size: ${props => props.size === 'large' ? '18px' :
props.size === 'small' ? '14px' : '16px'};
`;
const Button = ({ variant = 'default', size, children, ...rest }) => {
return (
<StyledButton variant={variant} size={size} {...rest}>
{children}
</StyledButton>
);
};
CSS Modules
.button {
padding: 10px 15px;
border-radius: 4px;
}
.primary {
background-color: #007bff;
color: #ffffff;
}
.danger {
background-color: #dc3545;
color: #ffffff;
}
.default {
background-color: #f8f9fa;
color: #212529;
}
.small {
font-size: 14px;
}
.large {
font-size: 18px;
}
import React from 'react';
import clsx from 'clsx';
import styles from './Button.module.css';
const Button = ({ variant = 'default', size, className, children, ...rest }) => {
const buttonClasses = clsx(
styles.button,
styles[variant],
{
[styles.small]: size === 'small',
[styles.large]: size === 'large'
},
className
);
return (
<button className={buttonClasses} {...rest}>
{children}
</button>
);
};
ここでのポイントはclsx
ライブラリの活用です。これにより条件付きのクラス名を効率的に組み合わせることができます。各スタイルのバリエーションを個別のクラスとして定義し、必要に応じて組み合わせるというアプローチが効果的です。
3. グローバルスタイル
styled-components
import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 1.5;
}
a {
color: #007bff;
text-decoration: none;
}
`;
import React from 'react';
import GlobalStyles from './GlobalStyles';
const App = () => (
<>
<GlobalStyles />
<div>コンテンツ</div>
</>
);
CSS Modules
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 16px;
line-height: 1.5;
}
a {
color: #007bff;
text-decoration: none;
}
import React from 'react';
import './global.css';
const App = () => (
<div>コンテンツ</div>
);
グローバルスタイルについては、通常のCSSファイルを使用することで対応できます。CSS Modulesはクラス名のみをスコープするため、セレクタを直接指定するグローバルスタイルは標準的なCSSファイルで問題なく機能します。
4. 変数を活用したテーマ管理
styled-components
export const theme = {
colors: {
primary: '#007bff',
text: '#333333'
},
fontSize: '16px',
padding: '10px'
};
import React from 'react';
import styled from 'styled-components';
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';
const StyledCard = styled.div`
background-color: white;
color: ${props => props.theme.colors.text};
font-size: ${props => props.theme.fontSize};
padding: ${props => props.theme.padding};
border: 1px solid ${props => props.theme.colors.primary};
`;
const Card = () => (
<ThemeProvider theme={theme}>
<StyledCard>
カード
</StyledCard>
</ThemeProvider>
);
CSS Modules
:root {
--color-primary: #007bff;
--color-text: #333333;
--font-size: 16px;
--padding: 10px;
}
.card {
background-color: white;
color: var(--color-text);
font-size: var(--font-size);
padding: var(--padding);
border: 1px solid var(--color-primary);
}
import React from 'react';
import styles from './Card.module.css';
const Card = () => (
<div className={styles.card}>
カード
</div>
);
CSS変数(カスタムプロパティ)を活用することで、styled-componentsのテーマと同様の仕組みを実現できます。:root
セレクタで定義するか、別のCSSファイルにまとめてインポートする方法で移行できます。
5. アニメーションとキーフレーム
styled-components
import styled, { keyframes } from 'styled-components';
const spin = keyframes`
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
`;
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
const StyledSpinner = styled.div`
width: ${props => props.size || '40px'};
height: ${props => props.size || '40px'};
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: ${props => props.color || '#007bff'};
border-radius: 50%;
animation: ${spin} 1s linear infinite, ${fadeIn} 0.3s ease-in;
`;
const SpinnerContainer = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const Spinner = ({ size, color, ...props }) => (
<SpinnerContainer>
<StyledSpinner size={size} color={color} {...props} />
<span>スピナー</span>
</SpinnerContainer>
);
CSS Modules
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #007bff;
border-radius: 50%;
animation: spin 1s linear infinite, fadeIn 0.3s ease-in;
}
.container {
display: flex;
align-items: center;
gap: 10px;
}
.primary {
border-left-color: var(--color-primary);
}
.small {
width: 24px;
height: 24px;
}
.large {
width: 64px;
height: 64px;
}
import React from 'react';
import clsx from 'clsx';
import styles from './Spinner.module.css';
const Spinner = ({ size, color, className, style = {}, ...props }) => {
const sizeClass = size === 'small' ? styles.small : size === 'large' ? styles.large : null;
const colorClass = color && styles[color] ? styles[color] : null;
// カスタムサイズや色の場合はインラインスタイルを使用
if (size && !sizeClass) {
style.width = size;
style.height = size;
}
if (color && !colorClass) {
style.borderLeftColor = color;
}
return (
<div className={styles.container}>
<div
className={clsx(styles.spinner, sizeClass, colorClass, className)}
style={style}
{...props}
/>
<span>スピナー</span>
</div>
);
};
6. メディアクエリの実装
styled-components
const ResponsiveContainer = styled.div`
padding: 20px;
@media (max-width: 768px) {
padding: 10px;
font-size: 14px;
}
@media (max-width: 480px) {
padding: 5px;
font-size: 12px;
}
`;
CSS Modules
.container {
padding: 20px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.container {
padding: 5px;
font-size: 12px;
}
}
CSS Modulesでもメディアクエリの実装は標準的なCSSと同様の記法で対応できます。
7. よくある課題と解決策
課題1: 動的スタイルの複雑性
課題: styled-componentsのprops経由のスタイル変更に比べて実装が複雑になる場合があります。
解決策: clsx
やclassnames
などのユーティリティライブラリを活用して、条件付きクラス名の管理を効率化できます。
import clsx from 'clsx';
const buttonClasses = clsx(
styles.button,
styles[variant],
{
[styles.small]: size === 'small',
[styles.large]: size === 'large',
[styles.disabled]: disabled
}
);
課題2: グローバルスタイルの管理
課題: ThemeProviderのような集中管理の仕組みがなくなります。
解決策: CSS変数を:root
セレクタで定義することで、集中管理を実現できます。
:root {
--color-primary: #007bff;
--color-secondary: #6c757d;
--spacing-unit: 8px;
--font-size-base: 16px;
}
課題3: TypeScriptとの連携
課題: CSSクラス名の型安全性の確保が課題になります。
解決策: typed-css-modules
などのツールを使用して、CSSモジュールの型定義を自動生成できます。
# インストール
npm install --save-dev typed-css-modules
# 実行
tcm src
まとめ
styled-componentsからCSS Modulesへの移行は、一見難しく感じるかもしれませんが、適切な方法で取り組むことで十分に実現可能です。
ぜひ移行の時に迷った際にはこの記事を参考にしていただければ幸いです。
Discussion