Skip to main content
Technology & EngineeringCss Styling Services391 lines

Styled Components

"styled-components: CSS-in-JS, tagged template literals, theming, dynamic styles, SSR, global styles, extending"

Quick Summary25 lines
styled-components uses tagged template literals to write actual CSS inside JavaScript. Each styled call creates a React component with automatically generated, unique class names. This eliminates class-name collisions, dead-code elimination is automatic (unused components are tree-shaken), and styles live next to the components they belong to. The library supports dynamic styling via props, theming through React context, and server-side rendering with style extraction. styled-components embraces the idea that styling is a concern of the component, not of a separate stylesheet.

## Key Points

1. **Use transient props ($ prefix).** Prefix props that are only for styling with `$` (e.g., `$intent`, `$size`) so they are not forwarded to the DOM and do not cause React warnings.
2. **Augment DefaultTheme in TypeScript.** Declare your theme shape in `styled.d.ts` so every `theme` access inside tagged templates is type-checked and autocompleted.
3. **Co-locate styles with components.** Define styled components in the same file or a sibling `*.styles.ts` file rather than a central stylesheet.
4. **Prefer extending over prop overloading.** Use `styled(Base)` to create specialized variants rather than adding more conditional props to a single component.
5. **Register styles for SSR.** In Next.js App Router, use `useServerInsertedHTML` with `ServerStyleSheet` to avoid flash of unstyled content.
6. **Use `css` helper for shared fragments.** Extract common CSS blocks with the `css` helper function and interpolate them across styled components.
2. **Using `attrs` for frequently changing values.** `attrs` for dynamic inline styles is fine, but using it for static configuration that should be props makes the API harder to understand.
3. **Deep prop-drilling for theme values.** Access theme values via the `theme` callback in tagged templates. Do not destructure the theme in parent components and pass individual values as props.

## Quick Example

```bash
npm install styled-components
npm install -D @types/styled-components
```

```bash
npm install -D babel-plugin-styled-components
```
skilldb get css-styling-services-skills/Styled ComponentsFull skill: 391 lines
Paste into your CLAUDE.md or agent config

styled-components

Core Philosophy

styled-components uses tagged template literals to write actual CSS inside JavaScript. Each styled call creates a React component with automatically generated, unique class names. This eliminates class-name collisions, dead-code elimination is automatic (unused components are tree-shaken), and styles live next to the components they belong to. The library supports dynamic styling via props, theming through React context, and server-side rendering with style extraction. styled-components embraces the idea that styling is a concern of the component, not of a separate stylesheet.

Setup

Installation

npm install styled-components
npm install -D @types/styled-components

Babel Plugin (Optional, Recommended)

npm install -D babel-plugin-styled-components
// .babelrc
{
  "plugins": [
    [
      "babel-plugin-styled-components",
      {
        "displayName": true,
        "ssr": true,
        "meaninglessFileNames": ["index", "styles"]
      }
    ]
  ]
}

Next.js Configuration

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  compiler: {
    styledComponents: true,
  },
};

export default nextConfig;

SSR Registry for Next.js App Router

// lib/styled-components-registry.tsx
"use client";

import React, { useState } from "react";
import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";

export function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  const [sheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = sheet.getStyleElement();
    sheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== "undefined") return <>{children}</>;

  return (
    <StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>
  );
}
// app/layout.tsx
import { StyledComponentsRegistry } from "@/lib/styled-components-registry";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

Key Techniques

Defining a Theme

// theme.ts
export const lightTheme = {
  colors: {
    brand: "#3b82f6",
    brandDark: "#1d4ed8",
    surface: "#ffffff",
    textPrimary: "#111827",
    textSecondary: "#6b7280",
    border: "#e5e7eb",
    danger: "#ef4444",
    success: "#22c55e",
  },
  space: {
    xs: "0.25rem",
    sm: "0.5rem",
    md: "1rem",
    lg: "1.5rem",
    xl: "2rem",
    "2xl": "3rem",
  },
  radii: {
    sm: "0.25rem",
    md: "0.5rem",
    lg: "0.75rem",
    full: "9999px",
  },
  fontSizes: {
    sm: "0.875rem",
    base: "1rem",
    lg: "1.125rem",
    xl: "1.25rem",
    "2xl": "1.5rem",
    "3xl": "1.875rem",
  },
  shadows: {
    sm: "0 1px 2px rgba(0,0,0,0.05)",
    md: "0 4px 6px rgba(0,0,0,0.07)",
    lg: "0 10px 15px rgba(0,0,0,0.1)",
  },
} as const;

export const darkTheme: typeof lightTheme = {
  colors: {
    brand: "#60a5fa",
    brandDark: "#3b82f6",
    surface: "#1f2937",
    textPrimary: "#f9fafb",
    textSecondary: "#9ca3af",
    border: "#374151",
    danger: "#f87171",
    success: "#4ade80",
  },
  space: lightTheme.space,
  radii: lightTheme.radii,
  fontSizes: lightTheme.fontSizes,
  shadows: {
    sm: "0 1px 2px rgba(0,0,0,0.3)",
    md: "0 4px 6px rgba(0,0,0,0.4)",
    lg: "0 10px 15px rgba(0,0,0,0.5)",
  },
};

export type AppTheme = typeof lightTheme;

TypeScript Theme Augmentation

// styled.d.ts
import "styled-components";
import type { AppTheme } from "./theme";

declare module "styled-components" {
  export interface DefaultTheme extends AppTheme {}
}

Theme Provider

"use client";

import { ThemeProvider } from "styled-components";
import { lightTheme, darkTheme } from "@/theme";
import { useThemeMode } from "@/hooks/useThemeMode";

export function AppThemeProvider({ children }: { children: React.ReactNode }) {
  const { mode } = useThemeMode();
  return (
    <ThemeProvider theme={mode === "dark" ? darkTheme : lightTheme}>
      {children}
    </ThemeProvider>
  );
}

Styled Components with Props

import styled from "styled-components";

interface ButtonProps {
  $intent?: "primary" | "secondary" | "danger";
  $size?: "sm" | "md" | "lg";
}

const sizeMap = {
  sm: { fontSize: "sm", px: "sm", py: "xs" },
  md: { fontSize: "base", px: "md", py: "sm" },
  lg: { fontSize: "lg", px: "lg", py: "md" },
} as const;

export const Button = styled.button<ButtonProps>`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: ${({ theme }) => theme.space.sm};
  font-weight: 500;
  border: none;
  border-radius: ${({ theme }) => theme.radii.md};
  cursor: pointer;
  transition: background-color 0.15s ease, box-shadow 0.15s ease;

  font-size: ${({ theme, $size = "md" }) => theme.fontSizes[sizeMap[$size].fontSize]};
  padding: ${({ theme, $size = "md" }) =>
    `${theme.space[sizeMap[$size].py]} ${theme.space[sizeMap[$size].px]}`};

  background-color: ${({ theme, $intent = "primary" }) => {
    if ($intent === "primary") return theme.colors.brand;
    if ($intent === "danger") return theme.colors.danger;
    return "transparent";
  }};

  color: ${({ theme, $intent = "primary" }) =>
    $intent === "secondary" ? theme.colors.textPrimary : "#fff"};

  border: ${({ theme, $intent }) =>
    $intent === "secondary" ? `1px solid ${theme.colors.border}` : "none"};

  &:hover:not(:disabled) {
    background-color: ${({ theme, $intent = "primary" }) => {
      if ($intent === "primary") return theme.colors.brandDark;
      if ($intent === "danger") return "#dc2626";
      return theme.colors.surface;
    }};
  }

  &:focus-visible {
    outline: 2px solid ${({ theme }) => theme.colors.brand};
    outline-offset: 2px;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

Extending Components

const Card = styled.div`
  background: ${({ theme }) => theme.colors.surface};
  border: 1px solid ${({ theme }) => theme.colors.border};
  border-radius: ${({ theme }) => theme.radii.lg};
  padding: ${({ theme }) => theme.space.lg};
  box-shadow: ${({ theme }) => theme.shadows.sm};
`;

const ElevatedCard = styled(Card)`
  box-shadow: ${({ theme }) => theme.shadows.lg};
  border: none;
  transition: transform 0.2s ease;

  &:hover {
    transform: translateY(-2px);
  }
`;

const CardTitle = styled.h3`
  font-size: ${({ theme }) => theme.fontSizes.xl};
  font-weight: 600;
  color: ${({ theme }) => theme.colors.textPrimary};
  margin-bottom: ${({ theme }) => theme.space.sm};
`;

const CardDescription = styled.p`
  font-size: ${({ theme }) => theme.fontSizes.base};
  color: ${({ theme }) => theme.colors.textSecondary};
  line-height: 1.6;
`;

Global Styles

import { createGlobalStyle } from "styled-components";

export const GlobalStyles = createGlobalStyle`
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  html {
    font-family: 'Inter', system-ui, -apple-system, sans-serif;
    -webkit-font-smoothing: antialiased;
  }

  body {
    background-color: ${({ theme }) => theme.colors.surface};
    color: ${({ theme }) => theme.colors.textPrimary};
    line-height: 1.5;
  }

  a {
    color: ${({ theme }) => theme.colors.brand};
    text-decoration: none;
    &:hover { text-decoration: underline; }
  }
`;

Composition Pattern

function ProfileCard({ user }: { user: User }) {
  return (
    <ElevatedCard>
      <AvatarRow>
        <Avatar src={user.avatar} alt={user.name} />
        <div>
          <CardTitle>{user.name}</CardTitle>
          <CardDescription>{user.bio}</CardDescription>
        </div>
      </AvatarRow>
      <Actions>
        <Button $intent="primary" $size="sm">Follow</Button>
        <Button $intent="secondary" $size="sm">Message</Button>
      </Actions>
    </ElevatedCard>
  );
}

const AvatarRow = styled.div`
  display: flex;
  align-items: center;
  gap: ${({ theme }) => theme.space.md};
`;

const Avatar = styled.img`
  width: 4rem;
  height: 4rem;
  border-radius: ${({ theme }) => theme.radii.full};
  object-fit: cover;
`;

const Actions = styled.div`
  display: flex;
  gap: ${({ theme }) => theme.space.sm};
  margin-top: ${({ theme }) => theme.space.lg};
`;

Best Practices

  1. Use transient props ($ prefix). Prefix props that are only for styling with $ (e.g., $intent, $size) so they are not forwarded to the DOM and do not cause React warnings.
  2. Augment DefaultTheme in TypeScript. Declare your theme shape in styled.d.ts so every theme access inside tagged templates is type-checked and autocompleted.
  3. Co-locate styles with components. Define styled components in the same file or a sibling *.styles.ts file rather than a central stylesheet.
  4. Prefer extending over prop overloading. Use styled(Base) to create specialized variants rather than adding more conditional props to a single component.
  5. Register styles for SSR. In Next.js App Router, use useServerInsertedHTML with ServerStyleSheet to avoid flash of unstyled content.
  6. Use css helper for shared fragments. Extract common CSS blocks with the css helper function and interpolate them across styled components.

Anti-Patterns

  1. Defining styled components inside render functions. This creates a new component class on every render, destroying React reconciliation and causing remounts. Always define styled components outside the component body.
  2. Using attrs for frequently changing values. attrs for dynamic inline styles is fine, but using it for static configuration that should be props makes the API harder to understand.
  3. Deep prop-drilling for theme values. Access theme values via the theme callback in tagged templates. Do not destructure the theme in parent components and pass individual values as props.
  4. Ignoring bundle size on RSC routes. styled-components requires a client-side runtime. In React Server Component architectures, this means wrapping pages in "use client" boundaries. Consider whether a zero-runtime alternative is more appropriate for server-heavy pages.
  5. Overusing createGlobalStyle. Global styles should be limited to CSS resets, font-face declarations, and root variables. Component-specific styles should always be scoped to a styled component.

Install this skill directly: skilldb add css-styling-services-skills

Get CLI access →