Skip to content

Fix: styled-components Not Working — Styles Not Applying, SSR Flash of Unstyled Content, or Theme Not Available

FixDevs ·

Quick Answer

How to fix styled-components issues — ThemeProvider setup, Next.js SSR with ServerStyleSheet, shouldForwardProp, attrs, TypeScript theme typing, and styled-components v6 migration.

The Problem

Styled components render with no styles or incorrect styles:

const Button = styled.button`
  background: blue;
  color: white;
  padding: 8px 16px;
`;
// <Button> renders as a plain button with no styles

Or theme values aren’t accessible inside a styled component:

const Text = styled.p`
  color: ${props => props.theme.colors.primary};
  // TypeError: Cannot read property 'colors' of undefined
`;

Or Next.js pages flash unstyled content on first load:

// Page loads with no styles for a fraction of a second, then styles appear

Or a custom prop causes a React warning:

Warning: Received `true` for a non-boolean attribute `isActive`.

Why This Happens

styled-components has specific setup requirements per environment:

  • ThemeProvider must wrap all styled components that use the theme — if a styled component using props.theme renders outside ThemeProvider, the theme is {} or undefined. Missing the provider at the app root is the most common cause.
  • SSR without ServerStyleSheet causes FOUC — styled-components generates class names at runtime. Without the SSR setup, styles are generated after the HTML is sent, causing a flash of unstyled content until JavaScript runs.
  • Custom props forwarded to DOM elements cause warnings — any prop you pass to a styled component that starts with a lowercase letter is forwarded to the underlying HTML element. Boolean or string custom props on DOM elements trigger React warnings. Use shouldForwardProp or prefix with $ (transient props in v5.1+).
  • CSS vendor prefixes — styled-components auto-applies them — if you’re seeing missing styles in older browsers, styled-components v5+ automatically prefixes. v6 removed the stylis CSS preprocessor by default for smaller bundle size.

Fix 1: Set Up ThemeProvider

// theme.ts — define your theme
export const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    background: '#ffffff',
    text: '#212529',
    error: '#dc3545',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
  },
  typography: {
    fontFamily: "'Inter', sans-serif",
    sizes: {
      sm: '14px',
      md: '16px',
      lg: '18px',
      xl: '24px',
    },
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
    full: '9999px',
  },
};

export type Theme = typeof theme;
// App.tsx — ThemeProvider must wrap everything
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';

function App() {
  return (
    <ThemeProvider theme={theme}>
      {/* All styled components here can access the theme */}
      <Router>
        <Layout />
      </Router>
    </ThemeProvider>
  );
}

TypeScript theme typing — avoid any in theme access:

// styled.d.ts — augment the DefaultTheme interface
import 'styled-components';
import { Theme } from './theme';

declare module 'styled-components' {
  export interface DefaultTheme extends Theme {}
}

// Now props.theme is fully typed
const Button = styled.button`
  background: ${props => props.theme.colors.primary};  // Typed!
  padding: ${props => props.theme.spacing.md};
`;

Fix 2: Fix Next.js SSR with styled-components

For Next.js App Router (recommended — use the compiler):

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  compiler: {
    styledComponents: true,  // Enable Next.js built-in styled-components support
  },
};

module.exports = nextConfig;

This handles SSR automatically without ServerStyleSheet. Restart the dev server after adding this.

For Next.js Pages Router (legacy approach with ServerStyleSheet):

// pages/_document.tsx
import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
        });

      const initialProps = await Document.getInitialProps(ctx);

      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Verify SSR is working:

# Check the HTML source — styles should be in a <style> tag in the <head>
curl http://localhost:3000 | grep styled

# Should see something like:
# <style data-styled="active" data-styled-version="6.x.x">...</style>

Fix 3: Fix Prop Forwarding Warnings

// WRONG — isActive is forwarded to <div> as a DOM attribute
const Panel = styled.div<{ isActive: boolean }>`
  background: ${props => props.isActive ? 'blue' : 'gray'};
`;
// Warning: Received `true` for a non-boolean attribute `isActive`

// FIX 1 — Transient props ($ prefix, v5.1+, doesn't forward to DOM)
const Panel = styled.div<{ $isActive: boolean }>`
  background: ${props => props.$isActive ? 'blue' : 'gray'};
`;
// Usage: <Panel $isActive={true}>

// FIX 2 — shouldForwardProp (explicit control)
const Panel = styled.div.withConfig({
  shouldForwardProp: (prop) => !['isActive', 'size', 'variant'].includes(prop),
})<{ isActive: boolean }>`
  background: ${props => props.isActive ? 'blue' : 'gray'};
`;

// FIX 3 — Destructure in attrs
const Panel = styled.div.attrs<{ isActive: boolean }>(props => ({
  // Map to data attributes or remove from forwarding
  'data-active': props.isActive,
}))`
  background: ${props => props.isActive ? 'blue' : 'gray'};
`;

Global shouldForwardProp configuration (v6):

import { StyleSheetManager, isPropValid } from 'styled-components';

function App() {
  return (
    <StyleSheetManager shouldForwardProp={isPropValid}>
      {/* isPropValid filters out non-HTML props automatically */}
      <YourApp />
    </StyleSheetManager>
  );
}

Fix 4: Dynamic Styles and Variants

// Button with multiple variants
interface ButtonProps {
  $variant?: 'primary' | 'secondary' | 'danger';
  $size?: 'sm' | 'md' | 'lg';
  $fullWidth?: boolean;
}

const Button = styled.button<ButtonProps>`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.15s;

  /* Size variants */
  ${({ $size = 'md' }) => {
    const sizes = {
      sm: 'height: 32px; padding: 0 12px; font-size: 13px;',
      md: 'height: 40px; padding: 0 16px; font-size: 14px;',
      lg: 'height: 48px; padding: 0 24px; font-size: 16px;',
    };
    return sizes[$size];
  }}

  /* Color variants */
  ${({ $variant = 'primary', theme }) => {
    const variants = {
      primary: `
        background: ${theme.colors.primary};
        color: white;
        &:hover { background: ${theme.colors.primary}dd; }
      `,
      secondary: `
        background: ${theme.colors.secondary};
        color: white;
        &:hover { background: ${theme.colors.secondary}dd; }
      `,
      danger: `
        background: ${theme.colors.error};
        color: white;
        &:hover { background: ${theme.colors.error}dd; }
      `,
    };
    return variants[$variant];
  }}

  ${({ $fullWidth }) => $fullWidth && 'width: 100%;'}

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

// Usage
<Button $variant="primary" $size="lg" onClick={handleClick}>
  Submit
</Button>

Using css helper for reusable style blocks:

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

const focusRing = css`
  outline: 2px solid ${props => props.theme.colors.primary};
  outline-offset: 2px;
`;

const Input = styled.input`
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 8px 12px;

  &:focus {
    ${focusRing}  // Reuse the style block
    border-color: ${props => props.theme.colors.primary};
  }
`;

// Conditional CSS block
const Card = styled.div<{ $elevated?: boolean }>`
  background: white;
  border-radius: 8px;

  ${({ $elevated }) => $elevated && css`
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transform: translateY(-2px);
  `}
`;

Fix 5: Global Styles and Keyframes

import { createGlobalStyle, keyframes } from 'styled-components';

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

  html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
  }

  body {
    font-family: ${({ theme }) => theme.typography.fontFamily};
    background-color: ${({ theme }) => theme.colors.background};
    color: ${({ theme }) => theme.colors.text};
    line-height: 1.5;
  }

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

// Add to App.tsx
function App() {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <Router>
        <Layout />
      </Router>
    </ThemeProvider>
  );
}

// Keyframes
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 LoadingSpinner = styled.div`
  width: 24px;
  height: 24px;
  border: 3px solid #f3f3f3;
  border-top: 3px solid ${props => props.theme.colors.primary};
  border-radius: 50%;
  animation: ${spin} 0.8s linear infinite;
`;

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

Fix 6: Migrate to styled-components v6

v6 introduced breaking changes from v5:

# Upgrade
npm install styled-components@latest

# Check for breaking changes
npx @styled-components/codemod v6/no-tags  # Auto-fix some patterns

Key v6 changes:

// 1. Stylis (CSS preprocessor) removed — no auto-vendor-prefixing
// Install if needed:
npm install @styled-components/stylis

// 2. No automatic type inference for theme
// Must use DefaultTheme augmentation (see Fix 1)

// 3. Tagged template literals with interpolated styled components
// v5 — works:
const Wrapper = styled.div`
  ${Button} { color: red; }
`;
// v6 — same, still works, but verify with your setup

// 4. transient props ($) — same as v5.1+
// No change

// 5. createGlobalStyle — minor API changes
// Test your global styles after upgrade

// 6. TypeScript — more strict types
// May need to update component prop types

Still Not Working?

Styles applied in dev but missing in production — class name hashing changed between environments. Enable deterministic class names:

// next.config.js
module.exports = {
  compiler: {
    styledComponents: {
      displayName: true,
      ssr: true,
      fileName: true,
      meaninglessFileNames: ['index'],
      minify: true,
      transpileTemplateLiterals: true,
    },
  },
};

Or via Babel:

// .babelrc
{
  "plugins": [
    ["babel-plugin-styled-components", {
      "ssr": true,
      "displayName": true,
      "preprocess": false
    }]
  ]
}

Multiple styled-components instances (causes theme loss) — if your app and a library both import styled-components, React may use two separate instances. Both must use the same instance:

# Check for duplicate instances
npm ls styled-components
# Should show only one version

# Fix with npm dedupe or by aliasing
npm dedupe

Styles lost after React hot reload — the Babel plugin or Next.js compiler handles this. Without it, class names regenerate on hot reload, breaking stylesheet injection. Verify the compiler config is active.

For related styling issues, see Fix: CSS Tailwind Not Applying and Fix: shadcn/ui Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles