styled-components-best-practices
ReactアプリケーションにおけるCSS-in-JS開発のための、styled-componentsのベストプラクティスを提供するスキルです。コンポーネントのスタイリング設計やパターン適用時に活用できます。
description の原文を見る
styled-components best practices for CSS-in-JS development in React applications
SKILL.md 本文
styled-components ベストプラクティス
styled-components、CSS-in-JS パターン、React コンポーネントのスタイリングに関する専門知識を持っています。
主な原則
- コンポーネントスコープのスタイルを記述して、グローバル CSS の競合を回避する
- JavaScript の全機能を活用して、動的なスタイリングを実現する
- styled コンポーネントは小さく、焦点を絞り、再利用可能に保つ
- 適切なメモ化と SSR サポートによるパフォーマンスを優先する
基本的なセットアップ
インストール
npm install styled-components
npm install -D @types/styled-components # TypeScript の場合
基本的な使い方
import styled from 'styled-components';
const Button = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color: #2980b9;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
// 使用法
function App() {
return <Button>Click me</Button>;
}
プロジェクト構造
ファイル構成
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.styles.ts # Styled コンポーネント
│ │ ├── Button.types.ts # TypeScript 型定義
│ │ └── index.ts # 再エクスポート
│ ├── Card/
│ │ ├── Card.tsx
│ │ ├── Card.styles.ts
│ │ └── index.ts
│ └── index.ts
├── styles/
│ ├── theme.ts # テーマ定義
│ ├── GlobalStyles.ts # グローバルスタイル
│ ├── mixins.ts # 再利用可能なスタイル mixin
│ └── index.ts
└── App.tsx
コンポーネントスタイルファイル
// Button.styles.ts
import styled, { css } from 'styled-components';
import type { ButtonProps } from './Button.types';
export const StyledButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: ${({ theme }) => theme.borderRadius.md};
font-family: inherit;
font-weight: ${({ theme }) => theme.fontWeight.medium};
cursor: pointer;
transition: all ${({ theme }) => theme.transition.base};
${({ size, theme }) => {
switch (size) {
case 'small':
return css`
padding: ${theme.spacing.xs} ${theme.spacing.sm};
font-size: ${theme.fontSize.small};
`;
case 'large':
return css`
padding: ${theme.spacing.md} ${theme.spacing.lg};
font-size: ${theme.fontSize.large};
`;
default:
return css`
padding: ${theme.spacing.sm} ${theme.spacing.md};
font-size: ${theme.fontSize.base};
`;
}
}}
${({ variant, theme }) => {
switch (variant) {
case 'secondary':
return css`
background-color: transparent;
color: ${theme.colors.primary};
border: 2px solid ${theme.colors.primary};
&:hover:not(:disabled) {
background-color: ${theme.colors.primary};
color: white;
}
`;
case 'danger':
return css`
background-color: ${theme.colors.error};
color: white;
&:hover:not(:disabled) {
background-color: ${theme.colors.errorDark};
}
`;
default:
return css`
background-color: ${theme.colors.primary};
color: white;
&:hover:not(:disabled) {
background-color: ${theme.colors.primaryDark};
}
`;
}
}}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
export const ButtonIcon = styled.span`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing.xs};
`;
テーマの設定
テーマ定義
// styles/theme.ts
export const theme = {
colors: {
primary: '#3498db',
primaryLight: '#5dade2',
primaryDark: '#2980b9',
secondary: '#2ecc71',
secondaryLight: '#58d68d',
secondaryDark: '#27ae60',
error: '#e74c3c',
errorLight: '#ec7063',
errorDark: '#c0392b',
warning: '#f39c12',
success: '#27ae60',
info: '#17a2b8',
text: '#333333',
textMuted: '#666666',
textLight: '#999999',
background: '#ffffff',
backgroundAlt: '#f8f9fa',
border: '#e0e0e0',
borderDark: '#cccccc',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px',
},
fontSize: {
xs: '0.75rem',
small: '0.875rem',
base: '1rem',
large: '1.25rem',
xl: '1.5rem',
xxl: '2rem',
xxxl: '2.5rem',
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
fontFamily: {
base: "'Helvetica Neue', Arial, sans-serif",
heading: "'Georgia', serif",
mono: "'Consolas', monospace",
},
lineHeight: {
tight: 1.2,
base: 1.5,
relaxed: 1.75,
},
borderRadius: {
sm: '2px',
md: '4px',
lg: '8px',
xl: '16px',
pill: '50px',
circle: '50%',
},
shadow: {
sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
md: '0 4px 6px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px rgba(0, 0, 0, 0.15)',
},
transition: {
fast: '0.15s ease',
base: '0.3s ease',
slow: '0.5s ease',
},
breakpoints: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
xxl: '1400px',
},
zIndex: {
dropdown: 1000,
sticky: 1020,
fixed: 1030,
modalBackdrop: 1040,
modal: 1050,
popover: 1060,
tooltip: 1070,
},
} as const;
export type Theme = typeof theme;
TypeScript テーマ型定義
// styles/styled.d.ts
import 'styled-components';
import type { Theme } from './theme';
declare module 'styled-components' {
export interface DefaultTheme extends Theme {}
}
Theme Provider セットアップ
// App.tsx
import { ThemeProvider } from 'styled-components';
import { theme } from './styles/theme';
import { GlobalStyles } from './styles/GlobalStyles';
function App() {
return (
<ThemeProvider theme={theme}>
<GlobalStyles />
{/* App content */}
</ThemeProvider>
);
}
グローバルスタイル
// styles/GlobalStyles.ts
import { createGlobalStyle } from 'styled-components';
export const GlobalStyles = createGlobalStyle`
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
font-family: ${({ theme }) => theme.fontFamily.base};
font-size: ${({ theme }) => theme.fontSize.base};
line-height: ${({ theme }) => theme.lineHeight.base};
color: ${({ theme }) => theme.colors.text};
background-color: ${({ theme }) => theme.colors.background};
}
h1, h2, h3, h4, h5, h6 {
font-family: ${({ theme }) => theme.fontFamily.heading};
font-weight: ${({ theme }) => theme.fontWeight.bold};
line-height: ${({ theme }) => theme.lineHeight.tight};
margin-top: 0;
margin-bottom: ${({ theme }) => theme.spacing.md};
}
p {
margin-top: 0;
margin-bottom: ${({ theme }) => theme.spacing.md};
}
a {
color: ${({ theme }) => theme.colors.primary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
button {
font-family: inherit;
}
img {
max-width: 100%;
height: auto;
}
/* アクセシビリティ用フォーカススタイル */
:focus-visible {
outline: 2px solid ${({ theme }) => theme.colors.primary};
outline-offset: 2px;
}
`;
動的スタイリング
Props ベースのスタイリング
import styled, { css } from 'styled-components';
interface CardProps {
$elevated?: boolean;
$variant?: 'default' | 'outlined' | 'filled';
}
const Card = styled.div<CardProps>`
border-radius: ${({ theme }) => theme.borderRadius.lg};
padding: ${({ theme }) => theme.spacing.md};
transition: box-shadow ${({ theme }) => theme.transition.base};
${({ $variant, theme }) => {
switch ($variant) {
case 'outlined':
return css`
background: transparent;
border: 1px solid ${theme.colors.border};
`;
case 'filled':
return css`
background: ${theme.colors.backgroundAlt};
border: none;
`;
default:
return css`
background: ${theme.colors.background};
border: 1px solid ${theme.colors.border};
`;
}
}}
${({ $elevated, theme }) =>
$elevated &&
css`
box-shadow: ${theme.shadow.md};
&:hover {
box-shadow: ${theme.shadow.lg};
}
`}
`;
// トランジェントプロップ ($プレフィックス) での使用
<Card $elevated $variant="outlined">Content</Card>
CSS ヘルパーの使用
import styled, { css } from 'styled-components';
// 再利用可能なスタイルブロック
const flexCenter = css`
display: flex;
align-items: center;
justify-content: center;
`;
const truncate = css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const visuallyHidden = css`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
const Container = styled.div`
${flexCenter}
min-height: 100vh;
`;
const Title = styled.h1`
${truncate}
max-width: 300px;
`;
const SrOnly = styled.span`
${visuallyHidden}
`;
コンポーネントの拡張
Styled コンポーネントの拡張
const Button = styled.button`
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
`;
const PrimaryButton = styled(Button)`
background: #3498db;
color: white;
&:hover {
background: #2980b9;
}
`;
const OutlinedButton = styled(Button)`
background: transparent;
color: #3498db;
border: 2px solid #3498db;
&:hover {
background: #3498db;
color: white;
}
`;
第三者コンポーネントの拡張
import { Link } from 'react-router-dom';
const StyledLink = styled(Link)`
color: ${({ theme }) => theme.colors.primary};
text-decoration: none;
font-weight: ${({ theme }) => theme.fontWeight.medium};
&:hover {
text-decoration: underline;
}
`;
レスポンシブデザイン
メディアクエリヘルパー
// styles/mixins.ts
import { css } from 'styled-components';
import type { Theme } from './theme';
type Breakpoint = keyof Theme['breakpoints'];
export const media = {
up: (breakpoint: Breakpoint) =>
(styles: ReturnType<typeof css>) => css`
@media (min-width: ${({ theme }) => theme.breakpoints[breakpoint]}) {
${styles}
}
`,
down: (breakpoint: Breakpoint) =>
(styles: ReturnType<typeof css>) => css`
@media (max-width: calc(${({ theme }) => theme.breakpoints[breakpoint]} - 1px)) {
${styles}
}
`,
};
// 使用法
const Container = styled.div`
padding: ${({ theme }) => theme.spacing.sm};
${({ theme }) => css`
@media (min-width: ${theme.breakpoints.md}) {
padding: ${theme.spacing.md};
}
@media (min-width: ${theme.breakpoints.lg}) {
padding: ${theme.spacing.lg};
}
`}
`;
レスポンシブコンポーネント
const Grid = styled.div`
display: grid;
gap: ${({ theme }) => theme.spacing.md};
grid-template-columns: 1fr;
@media (min-width: ${({ theme }) => theme.breakpoints.sm}) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: ${({ theme }) => theme.breakpoints.md}) {
grid-template-columns: repeat(3, 1fr);
}
@media (min-width: ${({ theme }) => theme.breakpoints.lg}) {
grid-template-columns: repeat(4, 1fr);
}
`;
アニメーション
キーフレーム
import styled, { keyframes } from 'styled-components';
const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const pulse = keyframes`
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
`;
const FadeInDiv = styled.div`
animation: ${fadeIn} 0.3s ease-out;
`;
const Spinner = styled.div`
width: 40px;
height: 40px;
border: 3px solid ${({ theme }) => theme.colors.border};
border-top-color: ${({ theme }) => theme.colors.primary};
border-radius: 50%;
animation: ${spin} 1s linear infinite;
`;
const PulsingDot = styled.span`
animation: ${pulse} 2s ease-in-out infinite;
`;
トランジショングループ
import styled from 'styled-components';
const Modal = styled.div<{ $isOpen: boolean }>`
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
opacity: ${({ $isOpen }) => ($isOpen ? 1 : 0)};
visibility: ${({ $isOpen }) => ($isOpen ? 'visible' : 'hidden')};
transition: opacity 0.3s ease, visibility 0.3s ease;
`;
const ModalContent = styled.div<{ $isOpen: boolean }>`
background: white;
padding: ${({ theme }) => theme.spacing.lg};
border-radius: ${({ theme }) => theme.borderRadius.lg};
transform: ${({ $isOpen }) => ($isOpen ? 'scale(1)' : 'scale(0.95)')};
transition: transform 0.3s ease;
`;
パフォーマンス最適化
静的スタイルの補間を回避
// 悪い例: 毎回のレンダリングで新しいクラスが作成される
const BadButton = styled.button`
padding: ${8}px ${16}px;
background: ${'#3498db'};
`;
// 良い例: 静的値は補間が不要
const GoodButton = styled.button`
padding: 8px 16px;
background: #3498db;
`;
// 良い例: テーマ値はキャッシュされる
const ThemedButton = styled.button`
padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md};
background: ${({ theme }) => theme.colors.primary};
`;
トランジェントプロップを使用
// DOM に渡されるべきではないプロップには $ プレフィックスを使用
interface StyledProps {
$isActive: boolean;
$size: 'small' | 'medium' | 'large';
}
const StyledDiv = styled.div<StyledProps>`
opacity: ${({ $isActive }) => ($isActive ? 1 : 0.5)};
padding: ${({ $size, theme }) =>
$size === 'small' ? theme.spacing.sm : theme.spacing.md};
`;
// $ プレフィックスの付いたプロップは DOM に表示されない
<StyledDiv $isActive={true} $size="medium" />
複雑なコンポーネントをメモ化
import { memo } from 'react';
import styled from 'styled-components';
const StyledCard = styled.div`
/* styles */
`;
interface CardProps {
title: string;
description: string;
}
const Card = memo(({ title, description }: CardProps) => (
<StyledCard>
<h2>{title}</h2>
<p>{description}</p>
</StyledCard>
));
SSR 設定
// Next.js の場合 - next.config.js
module.exports = {
compiler: {
styledComponents: true,
},
};
// その他のフレームワーク - ServerStyleSheet を使用
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
const sheet = new ServerStyleSheet();
try {
const html = renderToString(
<StyleSheetManager sheet={sheet.instance}>
<App />
</StyleSheetManager>
);
const styleTags = sheet.getStyleTags();
} finally {
sheet.seal();
}
ベストプラクティス
命名規則
// 明確さのために styled コンポーネントに接頭辞を付ける
export const StyledButton = styled.button``;
export const StyledCard = styled.div``;
// または説明的な名前を使用
export const ButtonWrapper = styled.div``;
export const CardContainer = styled.article``;
export const NavigationList = styled.ul``;
継承より合成
// 合成を優先
const BaseText = styled.p`
font-family: ${({ theme }) => theme.fontFamily.base};
line-height: ${({ theme }) => theme.lineHeight.base};
`;
const Heading = styled(BaseText).attrs({ as: 'h1' })`
font-size: ${({ theme }) => theme.fontSize.xxl};
font-weight: ${({ theme }) => theme.fontWeight.bold};
`;
const Caption = styled(BaseText)`
font-size: ${({ theme }) => theme.fontSize.small};
color: ${({ theme }) => theme.colors.textMuted};
`;
静的 Props に attrs を使用
const Input = styled.input.attrs(props => ({
type: props.type || 'text',
placeholder: props.placeholder || 'Enter text...',
}))`
padding: ${({ theme }) => theme.spacing.sm};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius.md};
&:focus {
border-color: ${({ theme }) => theme.colors.primary};
outline: none;
}
`;
アクセシビリティ
const IconButton = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px; /* 最小タッチターゲット */
height: 44px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
&:focus-visible {
outline: 2px solid ${({ theme }) => theme.colors.primary};
outline-offset: 2px;
}
`;
const VisuallyHidden = styled.span`
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
// 使用法
<IconButton aria-label="Close menu">
<CloseIcon />
<VisuallyHidden>Close menu</VisuallyHidden>
</IconButton>
テスト
Styled コンポーネントのテスト
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { theme } from './styles/theme';
import { Button } from './components/Button';
const renderWithTheme = (component: React.ReactElement) => {
return render(
<ThemeProvider theme={theme}>
{component}
</ThemeProvider>
);
};
describe('Button', () => {
it('renders with correct styles', () => {
renderWithTheme(<Button variant="primary">Click me</Button>);
const button = screen.getByRole('button');
expect(button).toHaveStyle({
backgroundColor: theme.colors.primary,
});
});
});
コードスタイル
- 1つの styled コンポーネント宣言ごとに1つ
- 順序: コンポーネント宣言、styled コンポーネント、型
- 複数行スタイルにはテンプレートリテラル構文を使用
- 再利用可能なスタイルブロックには css ヘルパーを使用
- トランジェントプロップには $ をプレフィックスとして使用
- styled コンポーネントを使用場所の近くに配置
- 共有スタイルを mixin またはテーマに抽出
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- mindrally
- リポジトリ
- mindrally/skills
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/mindrally/skills / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。