Skip to content

styled-components/jest-styled-components

Repository files navigation

NPM version CI

jest-styled-components

Testing utilities for styled-components (v5+). Works with Jest, Vitest, and Bun.

styled-components is largely maintained by one person. Please help fund the project for consistent long-term support and updates: Open Collective


The problem: styled-components snapshots contain opaque hashed class names and no CSS rules. When styles change, diffs show meaningless class name changes.

The solution: This library inlines actual CSS rules into snapshots with deterministic class placeholders (c0, c1, k0, k1) and provides a toHaveStyleRule matcher for asserting specific style values.

- Snapshot
+ Received

 .c0 {
-  color: green;
+  color: blue;
 }

 <button
   class="c0"
 />

Table of Contents

Installation

npm install --save-dev jest-styled-components
pnpm add -D jest-styled-components
yarn add -D jest-styled-components
bun add -D jest-styled-components

Setup

Import once in a setup file to register the snapshot serializer and toHaveStyleRule matcher globally.

Jest

// setupTests.js
import 'jest-styled-components'
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
}

Note: Jest 27+ defaults to the node environment. styled-components requires a DOM, so testEnvironment: 'jsdom' is required. You may also need to install jest-environment-jsdom separately for Jest 28+.

Vitest

// setupTests.ts
import 'jest-styled-components/vitest'
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['./setupTests.ts'],
  },
})

The Vitest entry point imports expect and beforeEach from Vitest explicitly. TypeScript types are included.

Bun

// setupTests.ts
import 'jest-styled-components'

Bun provides the expect and beforeEach globals that the library hooks into. Configure preload in your bunfig.toml:

[test]
preload = ["./setupTests.ts"]

Quick Example

import React from 'react'
import styled from 'styled-components'
import { render } from '@testing-library/react'
import 'jest-styled-components'

const Button = styled.button`
  color: red;
`

test('it works', () => {
  const { container } = render(<Button />)
  expect(container.firstChild).toMatchSnapshot()
  expect(container.firstChild).toHaveStyleRule('color', 'red')
})

Snapshot Testing

The serializer replaces hashed class names with sequential placeholders (c0, c1 for classes, k0, k1 for keyframes) and prepends the matching CSS rules to the snapshot output. This works with @testing-library/react, react-test-renderer, and Enzyme.

Theming

Wrap with ThemeProvider as you would in your app:

import { ThemeProvider } from 'styled-components'

const theme = { main: 'mediumseagreen' }

test('themed component', () => {
  const { container } = render(
    <ThemeProvider theme={theme}>
      <Button />
    </ThemeProvider>
  )
  expect(container.firstChild).toHaveStyleRule('color', 'mediumseagreen')
})

toHaveStyleRule

expect(element).toHaveStyleRule(property, value?, options?)

Asserts that a CSS property has the expected value on a styled component. The value argument accepts a string, RegExp, asymmetric matcher (e.g. expect.stringContaining()), or undefined to assert the property is not set. When used with .not, value is optional.

const Button = styled.button`
  color: red;
  border: 0.05em solid ${props => props.transparent ? 'transparent' : 'black'};
  cursor: ${props => !props.disabled && 'pointer'};
  opacity: ${props => props.disabled && '.65'};
`

test('default styles', () => {
  const { container } = render(<Button />)
  expect(container.firstChild).toHaveStyleRule('color', 'red')
  expect(container.firstChild).toHaveStyleRule('border', '0.05em solid black')
  expect(container.firstChild).toHaveStyleRule('cursor', 'pointer')
  expect(container.firstChild).not.toHaveStyleRule('opacity')
  expect(container.firstChild).toHaveStyleRule('opacity', undefined)
})

test('prop-dependent styles', () => {
  const { container } = render(<Button disabled transparent />)
  expect(container.firstChild).toHaveStyleRule('border', expect.stringContaining('transparent'))
  expect(container.firstChild).toHaveStyleRule('cursor', undefined)
  expect(container.firstChild).toHaveStyleRule('opacity', '.65')
})

Options

The third argument targets rules within at-rules, with selector modifiers, or by raw CSS selector.

Option Type Description
container string Match within a @container at-rule, e.g. '(min-width: 400px)'
layer string Match within a @layer at-rule, e.g. 'utilities'
media string Match within a @media at-rule, e.g. '(max-width: 640px)'
supports string Match within a @supports at-rule, e.g. '(display: grid)'
modifier string | css Refine the selector: pseudo-selectors, combinators, & references, or the css helper for component selectors
namespace string Match rules prefixed by a StyleSheetManager namespace, e.g. '#app'
selector string Match by raw CSS selector instead of component class. Useful for createGlobalStyle

media and modifier

const Button = styled.button`
  @media (max-width: 640px) {
    &:hover {
      color: red;
    }
  }
`

test('media + modifier', () => {
  const { container } = render(<Button />)
  expect(container.firstChild).toHaveStyleRule('color', 'red', {
    media: '(max-width:640px)',
    modifier: ':hover',
  })
})

supports

const Layout = styled.div`
  @supports (display: grid) {
    display: grid;
  }
`

test('supports query', () => {
  const { container } = render(<Layout />)
  expect(container.firstChild).toHaveStyleRule('display', 'grid', {
    supports: '(display:grid)',
  })
})

container

const Card = styled.div`
  container-type: inline-size;
  @container (min-width: 400px) {
    font-size: 1.5rem;
  }
`

test('container query', () => {
  const { container } = render(<Card />)
  expect(container.firstChild).toHaveStyleRule('font-size', '1.5rem', {
    container: '(min-width: 400px)',
  })
})

layer

const Themed = styled.div`
  @layer utilities {
    color: red;
  }
`

test('layer query', () => {
  const { container } = render(<Themed />)
  expect(container.firstChild).toHaveStyleRule('color', 'red', {
    layer: 'utilities',
  })
})

namespace (StyleSheetManager)

When using StyleSheetManager with a namespace prop, all CSS selectors are prefixed with the namespace. Pass namespace so the matcher knows to expect the prefix:

import { StyleSheetManager } from 'styled-components'

const Box = styled.div`
  color: blue;
`

test('namespaced styles', () => {
  const { container } = render(
    <StyleSheetManager namespace="#app">
      <Box />
    </StyleSheetManager>
  )
  expect(container.firstChild).toHaveStyleRule('color', 'blue', {
    namespace: '#app',
  })
})

To avoid passing namespace on every assertion, set it globally in your setup file:

import { setStyleRuleOptions } from 'jest-styled-components'

setStyleRuleOptions({ namespace: '#app' })

This applies to all subsequent toHaveStyleRule calls. Individual assertions can still override with their own namespace option.

modifier with component selectors

When a rule targets another styled-component, use the css helper:

import { css } from 'styled-components'

const Button = styled.button`
  color: red;
`

const ButtonList = styled.div`
  ${Button} {
    flex: 1 0 auto;
  }
`

test('nested component selector', () => {
  const { container } = render(<ButtonList><Button /></ButtonList>)
  expect(container.firstChild).toHaveStyleRule('flex', '1 0 auto', {
    modifier: css`${Button}`,
  })
})

Class name overrides work similarly:

const Button = styled.button`
  background-color: red;
  &.override {
    background-color: blue;
  }
`

test('class override', () => {
  const { container } = render(<Button className="override" />)
  expect(container.firstChild).toHaveStyleRule('background-color', 'blue', {
    modifier: '&.override',
  })
})

selector (createGlobalStyle)

The selector option matches rules by raw CSS selector, bypassing component class name detection. This is the way to test createGlobalStyle:

import { createGlobalStyle } from 'styled-components'

const GlobalStyle = createGlobalStyle`
  body {
    background: white;
  }
`

test('global styles', () => {
  render(<GlobalStyle />)
  expect(document.documentElement).toHaveStyleRule('background', 'white', {
    selector: 'body',
  })
})

When selector is set, the component argument is ignored---any rendered element will work as the receiver.

Element selection

The matcher checks styles on the element it receives. To assert on nested elements, query for them first:

const { getByTestId } = render(<MyComponent />)
expect(getByTestId('inner-button')).toHaveStyleRule('color', 'blue')

React Native

Import the native entry point instead:

import 'jest-styled-components/native'

This registers only the toHaveStyleRule matcher adapted for React Native's style objects (no snapshot serializer needed). It handles style arrays and converts kebab-case properties to camelCase.

import React from 'react'
import styled from 'styled-components/native'
import renderer from 'react-test-renderer'
import 'jest-styled-components/native'

const Label = styled.Text`
  color: green;
`

test('native styles', () => {
  const tree = renderer.create(<Label />).toJSON()
  expect(tree).toHaveStyleRule('color', 'green')
})

Advanced Usage

Standalone Serializer

The serializer can be imported separately for use with libraries like jest-specific-snapshot:

import { styleSheetSerializer } from 'jest-styled-components/serializer'
import { addSerializer } from 'jest-specific-snapshot'

addSerializer(styleSheetSerializer)

Serializer Options

import { setStyleSheetSerializerOptions } from 'jest-styled-components/serializer'

setStyleSheetSerializerOptions({
  addStyles: false,                          // omit CSS from snapshots
  classNameFormatter: (index) => `styled${index}`,  // custom class placeholders
})

CSS Parse Caching

By default, toHaveStyleRule re-parses the stylesheet on every assertion. For test suites with many assertions, import the cached entry point to parse once and reuse the result when the stylesheet hasn't changed:

import 'jest-styled-components/cache'

The cache automatically invalidates when the stylesheet changes and when resetStyleSheet runs between tests. No manual cleanup needed.

resetStyleSheet

The main entry point calls resetStyleSheet() in a beforeEach hook automatically. If you use the standalone serializer or a custom test setup where beforeEach is not globally available, call it manually:

import { resetStyleSheet } from 'jest-styled-components'

resetStyleSheet()

Version Compatibility

jest-styled-components styled-components Test Runner
7.2+ 5.x, 6.x Jest 27+, Vitest 1+, Bun
7.0--7.1 5.x Jest 27+
6.x 4.x--5.x Jest 24--26

Troubleshooting

"No style rules found on passed Component"

The most common issue. Check these causes in order:

  1. Wrong element. The matcher needs the actual DOM element or react-test-renderer JSON node, not a wrapper. With @testing-library/react, use container.firstChild. With Enzyme, use mount() (not shallow() for most cases).
  2. Multiple styled-components instances. Common in monorepos. Run npm ls styled-components (or pnpm why styled-components) to check. See the styled-components FAQ for resolution.
  3. Version mismatch. Ensure your jest-styled-components version supports your styled-components version (see Version Compatibility).

TypeScript: toHaveStyleRule not recognized

The package ships type declarations that augment Jest's Matchers interface. If TypeScript doesn't pick them up:

Jest: Ensure your tsconfig.json includes the package in types, or that your setup file import (import 'jest-styled-components') is within the TypeScript project's include paths.

Vitest: Import from jest-styled-components/vitest (not the base entry point). This augments Vitest's Assertion interface.

Styles missing from snapshots

If snapshots show hashed class names instead of CSS rules, the serializer isn't registered. Verify:

  1. You're importing jest-styled-components (or jest-styled-components/vitest for Vitest) in your setup file.
  2. The setup file is referenced in your test runner config (setupFilesAfterEnv for Jest, setupFiles for Vitest).
  3. You don't have multiple instances of styled-components loaded (see above).

Testing createGlobalStyle

Global styles don't attach to a specific component. Use the selector option to match by CSS selector:

render(<GlobalStyle />)
expect(document.documentElement).toHaveStyleRule('background', 'white', {
  selector: 'body',
})

Jest environment errors (document is not defined)

Jest 27+ defaults to the node test environment. Add testEnvironment: 'jsdom' to your Jest config. For Jest 28+, install jest-environment-jsdom as a separate dependency.

Legacy: Enzyme

Enzyme is no longer actively maintained. If you still use it, snapshot testing requires enzyme-to-json and toHaveStyleRule works with both shallow and mounted wrappers. Consider migrating to @testing-library/react.

Contributing

Open an issue to discuss before submitting a PR.

License

Licensed under the MIT License.