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"
/>- Installation
- Setup
- Quick Example
- Snapshot Testing
- toHaveStyleRule
- React Native
- Advanced Usage
- Version Compatibility
- Troubleshooting
- Legacy: Enzyme
npm install --save-dev jest-styled-componentspnpm add -D jest-styled-componentsyarn add -D jest-styled-componentsbun add -D jest-styled-componentsImport once in a setup file to register the snapshot serializer and toHaveStyleRule matcher globally.
// 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+.
// 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.
// 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"]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')
})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.
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')
})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')
})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 |
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',
})
})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)',
})
})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)',
})
})const Themed = styled.div`
@layer utilities {
color: red;
}
`
test('layer query', () => {
const { container } = render(<Themed />)
expect(container.firstChild).toHaveStyleRule('color', 'red', {
layer: 'utilities',
})
})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.
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',
})
})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.
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')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')
})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)import { setStyleSheetSerializerOptions } from 'jest-styled-components/serializer'
setStyleSheetSerializerOptions({
addStyles: false, // omit CSS from snapshots
classNameFormatter: (index) => `styled${index}`, // custom class placeholders
})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.
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()| 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 |
The most common issue. Check these causes in order:
- Wrong element. The matcher needs the actual DOM element or react-test-renderer JSON node, not a wrapper. With
@testing-library/react, usecontainer.firstChild. With Enzyme, usemount()(notshallow()for most cases). - Multiple styled-components instances. Common in monorepos. Run
npm ls styled-components(orpnpm why styled-components) to check. See the styled-components FAQ for resolution. - Version mismatch. Ensure your jest-styled-components version supports your styled-components version (see Version Compatibility).
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.
If snapshots show hashed class names instead of CSS rules, the serializer isn't registered. Verify:
- You're importing
jest-styled-components(orjest-styled-components/vitestfor Vitest) in your setup file. - The setup file is referenced in your test runner config (
setupFilesAfterEnvfor Jest,setupFilesfor Vitest). - You don't have multiple instances of styled-components loaded (see above).
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 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.
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.
Open an issue to discuss before submitting a PR.
Licensed under the MIT License.