mcpField
S
styled-components-best-practices

styled-components-best-practices

github.com/mindrally设计与 UI287768 次安装3 周前更新
代码仓库

关于 styled-components-best-practices

styled-components-best-practices 是面向使用 styled-components 为 React 组件编写样式的开发者的指南技能,涵盖组件样式、主题、响应式布局、性能、SSR、无障碍和测试。

主要功能

  1. 01作用域化与可复用组件样式
  2. 02主题与 TypeScript 配置
  3. 03动态和响应式样式
  4. 04性能与 SSR 指南
  5. 05无障碍和测试实践

适用场景

  1. 01组织 React 组件样式
  2. 02构建支持主题的组件库
  3. 03创建响应式 styled components
  4. 04检查 SSR 与无障碍实践

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mindrally/skills --skill styled-components-best-practices

安装后在 Claude Code 中输入/styled-components-best-practices确认已加载。

详细介绍

styled-components 最佳实践

你是 styled-components、CSS-in-JS 模式和 React 组件样式方面的专家。

核心原则

  • 编写组件作用域内的样式,避免全局 CSS 冲突
  • 充分发挥 JavaScript 在动态样式方面的强大能力
  • 保持样式化组件小巧、专注且可复用
  • 通过恰当的记忆化和 SSR 支持,优先保障性能

基本设置

安装
npm install styled-components
npm install -D @types/styled-components  # For 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;
  }
`;

// Usage
function App() {
  return <Button>Click me</Button>;
}

项目结构

文件组织
src/
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.styles.ts    # Styled components
│   │   ├── Button.types.ts     # TypeScript types
│   │   └── index.ts            # Re-exports
│   ├── Card/
│   │   ├── Card.tsx
│   │   ├── Card.styles.ts
│   │   └── index.ts
│   └── index.ts
├── styles/
│   ├── theme.ts                # Theme definition
│   ├── GlobalStyles.ts         # Global styles
│   ├── mixins.ts               # Reusable style mixins
│   └── 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 {}
}
主题提供器设置
// 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 styles for accessibility */
  :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};
      }
    `}
`;

// Usage with transient props ($prefix)
<Card $elevated $variant="outlined">Content</Card>
使用 CSS 辅助函数
import styled, { css } from 'styled-components';

// Reusable style blocks
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}
`;

扩展组件

扩展样式化组件
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}
      }
    `,
};

// Usage
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;
`;

性能优化

避免在静态样式中使用插值
// BAD: Creates new class on every render
const BadButton = styled.button`
  padding: ${8}px ${16}px;
  background: ${'#3498db'};
`;

// GOOD: Static values don't need interpolation
const GoodButton = styled.button`
  padding: 8px 16px;
  background: #3498db;
`;

// GOOD: Theme values are cached
const ThemedButton = styled.button`
  padding: ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md};
  background: ${({ theme }) => theme.colors.primary};
`;
使用瞬态 props
// Use $ prefix for props that shouldn't be passed to 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};
`;

// Props with $ prefix won't appear in 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 配置
// For Next.js - next.config.js
module.exports = {
  compiler: {
    styledComponents: true,
  },
};

// For other frameworks - use 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();
}

最佳实践

命名约定
// Prefix styled components for clarity
export const StyledButton = styled.button``;
export const StyledCard = styled.div``;

// Or use descriptive names
export const ButtonWrapper = styled.div``;
export const CardContainer = styled.article``;
export const NavigationList = styled.ul``;
优先组合,而非继承
// Prefer composition
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};
`;
使用 attrs 设置静态 props
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;  /* Minimum touch target */
  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;
`;

// Usage
<IconButton aria-label="Close menu">
  <CloseIcon />
  <VisuallyHidden>Close menu</VisuallyHidden>
</IconButton>

测试

测试样式化组件
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,
    });
  });
});

代码风格

  • 每个声明定义一个样式化组件
  • 顺序:组件声明、样式化组件、类型
  • 多行样式使用模板字面量语法
  • 使用 css 辅助函数复用样式块
  • 瞬态 props 使用 $ 前缀
  • 将样式化组件放在靠近其使用位置的地方
  • 将共享样式提取到混入或主题中

评论

支持 Markdown