NexusCS

Styled Components

React
CSS-in-JS library for React and React Native. Write component-scoped CSS with template literals, dynamic theming, and TypeScript support.
css-in-js
react
styling
typescript

Getting started

Installation

npm install styled-components
yarn add styled-components
pnpm add styled-components

v6+ includes TypeScript types by default.

Basic usage

import styled from "styled-components";

const Button = styled.button`
  background: #bf4f74;
  border-radius: 3px;
  border: none;
  color: white;
  padding: 0.25em 1em;
`;

// Usage
<Button>Click me</Button>;

Creates a React component with scoped styles.

Quick example

const Container = styled.div`
  text-align: center;
  padding: 2rem;
`;

const Title = styled.h1`
  font-size: 2.5em;
  color: #bf4f74;
`;

function App() {
  return (
    <Container>
      <Title>Hello World</Title>
    </Container>
  );
}

Dynamic styling

Props-based styles

const Button = styled.button<{ $primary?: boolean }>`
  background: ${props =>
    props.$primary ? '#BF4F74' : 'white'
  };
  color: ${props =>
    props.$primary ? 'white' : '#BF4F74'
  };
  border: 2px solid #BF4F74;
`;

// Usage
<Button $primary>Primary</Button>
<Button>Normal</Button>

Use $ prefix for transient props (v5.1+).

Transient props

const Input = styled.input<{ $error?: boolean }>`
  border-color: ${(p) => (p.$error ? "red" : "#ccc")};
`;

// $error won't be forwarded to DOM
<Input $error type="text" />;

Prevents invalid DOM attribute warnings.

Prop interpolation

const Box = styled.div<{
  $size?: number;
  $bg?: string;
}>`
  width: ${(p) => p.$size || 100}px;
  height: ${(p) => p.$size || 100}px;
  background: ${(p) => p.$bg || "blue"};
`;

<Box $size={200} $bg="tomato" />;

Extending styles

Styled extension

const Button = styled.button`
  color: #bf4f74;
  border: 2px solid #bf4f74;
  padding: 0.25em 1em;
`;

const TomatoButton = styled(Button)`
  color: tomato;
  border-color: tomato;
`;

Override styles in child component.

Polymorphic "as" prop

const Button = styled.button`
  padding: 0.5em 1em;
  border: 2px solid #BF4F74;
`;

// Render as different element
<Button as="a" href="/page">
  Link Button
</Button>

<Button as="div">
  Div Button
</Button>

Extending custom components

const Link = ({ className, children }) => (
  <a className={className}>{children}</a>
);

const StyledLink = styled(Link)`
  color: #bf4f74;
  font-weight: bold;
`;

Component must accept className prop.

Pseudo-selectors & nesting

Pseudo-selectors

const Thing = styled.div`
  color: blue;

  &:hover {
    color: red;
  }

  &::before {
    content: "🎨";
  }

  &:first-child {
    margin-top: 0;
  }
`;

Always use & prefix (v6 requirement).

Nesting & combinators

const Article = styled.article`
  & > p {
    margin-bottom: 1em;
  }

  & + & {
    margin-top: 2em; /* Adjacent sibling */
  }

  .child-class {
    border: 1px solid #ccc;
  }
`;

Targeting child elements

const Container = styled.div`
  padding: 1rem;

  h2 {
    color: #bf4f74;
  }

  p {
    line-height: 1.6;
  }
`;

Media queries

Basic media queries

const Container = styled.div`
  background: papayawhip;

  @media (min-width: 768px) {
    background: mediumseagreen;
  }

  @media (min-width: 1024px) {
    background: rebeccapurple;
  }
`;

Reusable breakpoints

const sizes = {
  mobile: "320px",
  tablet: "768px",
  desktop: "1024px",
};

const media = {
  mobile: `@media (min-width: ${sizes.mobile})`,
  tablet: `@media (min-width: ${sizes.tablet})`,
  desktop: `@media (min-width: ${sizes.desktop})`,
};

const Box = styled.div`
  width: 100%;

  ${media.tablet} {
    width: 50%;
  }

  ${media.desktop} {
    width: 33.33%;
  }
`;

Container queries

const Card = styled.div`
  container-type: inline-size;

  h2 {
    font-size: 1.5rem;
  }

  @container (min-width: 400px) {
    h2 {
      font-size: 2rem;
    }
  }
`;

Theming

ThemeProvider

import { ThemeProvider } from "styled-components";

const theme = {
  colors: {
    primary: "#BF4F74",
    secondary: "mediumseagreen",
  },
  fonts: {
    body: "Helvetica, sans-serif",
  },
};

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>Themed Button</Button>
    </ThemeProvider>
  );
}

Using theme

const Button = styled.button`
  color: ${(props) => props.theme.colors.primary};
  font-family: ${(props) => props.theme.fonts.body};
  background: ${({ theme }) => theme.colors.secondary};
`;

Function themes

// Merge with parent theme
<ThemeProvider theme={outerTheme => ({
  ...outerTheme,
  colors: {
    ...outerTheme.colors,
    primary: 'blue',
  },
})}>
  <Content />
</ThemeProvider>

// Invert theme
<ThemeProvider theme={({ fg, bg }) => ({ fg: bg, bg: fg })}>
  <InvertedSection />
</ThemeProvider>

useTheme hook

import { useTheme } from "styled-components";

function MyComponent() {
  const theme = useTheme();

  return (
    <div style={{ color: theme.colors.primary }}>
      Current theme: {theme.name}
    </div>
  );
}

TypeScript

Declare theme type

// styled.d.ts
import "styled-components";

declare module "styled-components" {
  export interface DefaultTheme {
    borderRadius: string;
    colors: {
      primary: string;
      secondary: string;
    };
  }
}

Place in project root or src/.

Type component props

interface ButtonProps {
  $primary?: boolean;
  $size?: "small" | "medium" | "large";
}

const Button = styled.button<ButtonProps>`
  background: ${(p) => (p.$primary ? "blue" : "white")};
  padding: ${(p) => {
    switch (p.$size) {
      case "small":
        return "0.25em 0.5em";
      case "large":
        return "0.75em 1.5em";
      default:
        return "0.5em 1em";
    }
  }};
`;

Type theme access

const Heading = styled.h1`
  color: ${({ theme }) => theme.colors.primary};
  font-size: ${({ theme }) => theme.fontSizes.xl};
`;
// theme is fully typed via DefaultTheme

Animations

keyframes

import styled, { keyframes } from "styled-components";

const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`;

const Spinner = styled.div`
  animation: ${rotate} 2s linear infinite;
  width: 50px;
  height: 50px;
`;

Multiple animations

const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`;

const slideUp = keyframes`
  from { transform: translateY(20px); }
  to { transform: translateY(0); }
`;

const Box = styled.div`
  animation:
    ${fadeIn} 0.3s ease-out,
    ${slideUp} 0.3s ease-out;
`;

Conditional animations

const pulse = keyframes`
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
`;

const Button = styled.button<{ $loading?: boolean }>`
  animation: ${(p) => (p.$loading ? pulse : "none")} 1s infinite;
`;

Global styles

createGlobalStyle

import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }
  
  body {
    font-family: 'Helvetica Neue', sans-serif;
    line-height: 1.6;
    color: #333;
  }
  
  a {
    text-decoration: none;
    color: inherit;
  }
`;

// Usage
function App() {
  return (
    <>
      <GlobalStyle />
      <Content />
    </>
  );
}

Global styles with props

const GlobalStyle = createGlobalStyle<{ $whiteColor?: boolean }>`
  body {
    color: ${(props) => (props.$whiteColor ? "white" : "black")};
    background: ${(props) => (props.$whiteColor ? "black" : "white")};
  }
`;

<GlobalStyle $whiteColor />;

Global styles with theme

const GlobalStyle = createGlobalStyle`
  body {
    font-family: ${({ theme }) => theme.fonts.body};
    background: ${({ theme }) => theme.colors.background};
    color: ${({ theme }) => theme.colors.text};
  }
`;

<ThemeProvider theme={theme}>
  <GlobalStyle />
  <App />
</ThemeProvider>;

Advanced patterns

.attrs()

// Static attributes
const Input = styled.input.attrs({
  type: "text",
  placeholder: "Enter text...",
})`
  border: 2px solid #bf4f74;
  padding: 0.5em;
`;

// Dynamic attributes
const Input = styled.input.attrs<{ $size?: string }>((props) => ({
  type: "text",
  size: props.$size || "1em",
}))`
  padding: ${(props) => props.size};
`;

Avoid re-rendering for static values.

css helper

import styled, { css } from "styled-components";

const sharedStyles = css`
  background: papayawhip;
  color: #bf4f74;
  padding: 1rem;
`;

const Box = styled.div`
  ${sharedStyles}
  border: 1px solid;
`;

const Card = styled.article`
  ${sharedStyles}
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;

css with props

const textStyles = css<{ $large?: boolean }>`
  font-size: ${(p) => (p.$large ? "2rem" : "1rem")};
  font-weight: ${(p) => (p.$large ? "bold" : "normal")};
`;

const Heading = styled.h1<{ $large?: boolean }>`
  ${textStyles}
  color: #BF4F74;
`;

Referring to other components

const Icon = styled.svg`
  width: 24px;
  fill: #bf4f74;
  transition: fill 0.2s;
`;

const Link = styled.a`
  display: flex;
  align-items: center;
  gap: 0.5rem;

  &:hover ${Icon} {
    fill: rebeccapurple;
  }
`;

// Usage
<Link href="/home">
  <Icon />
  Home
</Link>;

shouldForwardProp

const Comp = styled("div").withConfig({
  shouldForwardProp: (prop) => !["hidden", "active"].includes(prop),
})<{ hidden?: boolean; active?: boolean }>`
  display: ${(p) => (p.hidden ? "none" : "block")};
  opacity: ${(p) => (p.active ? 1 : 0.5)};
`;

Prevent custom props from reaching DOM.

Style objects

// Object syntax alternative
const Box = styled.div({
  background: "#BF4F74",
  height: "50px",
  width: "50px",
});

// With props
const PropsBox = styled.div<{ $bg?: string }>((props) => ({
  background: props.$bg || "blue",
  padding: "1rem",
}));

Server-side rendering

SSR setup

import { ServerStyleSheet } from "styled-components";
import { renderToString } from "react-dom/server";

const sheet = new ServerStyleSheet();

try {
  const html = renderToString(sheet.collectStyles(<App />));
  const styleTags = sheet.getStyleTags();

  // Inject styleTags into <head>
  const fullHtml = `
    <!DOCTYPE html>
    <html>
      <head>${styleTags}</head>
      <body><div id="root">${html}</div></body>
    </html>
  `;
} catch (error) {
  console.error(error);
} finally {
  sheet.seal();
}

Next.js integration

// next.config.js
module.exports = {
  compiler: {
    styledComponents: true,
  },
};

Enables SWC transformation for styled-components.

Next.js App Router (v6.3.0+)

// Works in React Server Components
const ServerComponent = styled.div`
  color: red;
  padding: 1rem;
`;

// For theming, use CSS custom properties
const Button = styled.button`
  background: var(--color-primary, blue);
  color: white;
`;

// In RSC
<div style={{ "--color-primary": "orchid" }}>
  <Button>Themed Button</Button>
</div>;

No 'use client' needed for basic styles.

StyleSheetManager

Vendor prefixes

import { StyleSheetManager } from "styled-components";

function App() {
  return (
    <StyleSheetManager enableVendorPrefixes>
      <Content />
    </StyleSheetManager>
  );
}

Disabled by default in v6.

RTL support

import { StyleSheetManager } from "styled-components";
import stylisRTLPlugin from "stylis-plugin-rtl";

function App() {
  return (
    <StyleSheetManager stylisPlugins={[stylisRTLPlugin]}>
      <Content />
    </StyleSheetManager>
  );
}

Custom stylis plugins

import { StyleSheetManager } from "styled-components";

const customPlugin = (context, content) => {
  // Custom CSS transformation
};

<StyleSheetManager stylisPlugins={[customPlugin]}>
  <App />
</StyleSheetManager>;

Common gotchas

Never define inside render

// ❌ BAD - Creates new component every render
function MyComponent() {
  const Heading = styled.h1`
    color: red;
  `;
  return <Heading>Title</Heading>;
}

// ✅ GOOD - Define outside
const Heading = styled.h1`
  color: red;
`;

function MyComponent() {
  return <Heading>Title</Heading>;
}

Breaks reconciliation and remounts component.

Use transient props

// ❌ BAD - Warning in console
const Div = styled.div<{ hidden: boolean }>`
  display: ${(p) => (p.hidden ? "none" : "block")};
`;
<Div hidden={true} />; // hidden goes to DOM

// ✅ GOOD - Use $ prefix
const Div = styled.div<{ $hidden: boolean }>`
  display: ${(p) => (p.$hidden ? "none" : "block")};
`;
<Div $hidden={true} />; // $hidden filtered out

v6 pseudo-selector changes

// ❌ BAD - Treated as child selector in v6
const Div = styled.div`
  :hover {
    color: red; /* Selects :hover children! */
  }
`;

// ✅ GOOD - Always use &
const Div = styled.div`
  &:hover {
    color: red; /* Selects self on hover */
  }
`;

Custom components need className

// ❌ BAD - Styles won't apply
const MyComponent = ({ children }) => <div>{children}</div>;
const Styled = styled(MyComponent)`
  color: red;
`;

// ✅ GOOD - Forward className
const MyComponent = ({ className, children }) => (
  <div className={className}>{children}</div>
);
const Styled = styled(MyComponent)`
  color: red;
`;

keyframes in shared fragments

// ❌ BAD - Won't work in v4+
const rotate = keyframes`...`;
const styles = `animation: ${rotate} 2s;`;

// ✅ GOOD - Use css helper
import { css, keyframes } from "styled-components";
const rotate = keyframes`...`;
const styles = css`
  animation: ${rotate} 2s;
`;

Also see