A set of utilities for testing Styled Components (v5+) with Jest. Provides a snapshot serializer that inlines CSS into snapshots and a toHaveStyleRule matcher for asserting style rules.
npm install --save-dev jest-styled-componentsimport 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')
})To avoid importing in every test file, use the global setup method.
Without this package, styled-components snapshots contain opaque hashed class names and no CSS rules. Changes to styles only show up as class name diffs, which is uninformative.
After importing jest-styled-components, snapshots include the actual CSS rules and use deterministic class name placeholders (c0, c1, etc.), producing clear diffs π:
- Snapshot
+ Received
.c0 {
- color: green;
+ color: blue;
}
<button
class="c0"
/>import { render } from '@testing-library/react'
test('it works', () => {
const { container } = render(<Button />)
expect(container.firstChild).toMatchSnapshot()
})Snapshots from DOM elements use class instead of className.
import renderer from 'react-test-renderer'
test('it works', () => {
const tree = renderer.create(<Button />).toJSON()
expect(tree).toMatchSnapshot()
})Pass the theme directly as a prop or wrap with ThemeProvider:
import { ThemeProvider } from 'styled-components'
const theme = { main: 'mediumseagreen' }
test('with theme prop', () => {
const { container } = render(<Button theme={theme} />)
expect(container.firstChild).toHaveStyleRule('color', 'mediumseagreen')
})
test('with ThemeProvider', () => {
const { container } = render(
<ThemeProvider theme={theme}>
<Button />
</ThemeProvider>
)
expect(container.firstChild).toHaveStyleRule('color', 'mediumseagreen')
})Asserts that a CSS property has the expected value. The second argument accepts a string, RegExp, Jest asymmetric matcher, or undefined. When used with .not, the second argument 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 is an options object for targeting rules within at-rules, with 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 |
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 a rule is nested within another styled-component, use the css helper to target it:
const Button = styled.button`
color: red;
`
const ButtonList = styled.div`
${Button} {
flex: 1 0 auto;
}
`
import { css } from 'styled-components'
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 to toHaveStyleRule is ignored -- any rendered element will work as the receiver.
The matcher checks styles on the root 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 Vitest-specific entry point in your setup file:
import 'jest-styled-components/vitest'This registers the serializer, matcher, and stylesheet reset using Vitest's expect and beforeEach. TypeScript types are included at jest-styled-components/vitest.
Configure in vitest.config.ts:
export default defineConfig({
test: {
setupFiles: ['./src/setupTests.ts'],
},
})Works with Bun's test runner out of the box. Import jest-styled-components in your test setup as usual -- Bun provides the expect and beforeEach globals that the library hooks into.
For 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')
})To avoid importing in every test file, create a setup file:
// src/setupTests.js
import 'jest-styled-components'Then add it to your Jest config:
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
}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
})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'
// In your test setup or beforeEach
resetStyleSheet()By default, toHaveStyleRule re-parses the stylesheet on every assertion. For test suites with many assertions, this can be slow. Import the cached entry point to parse once and reuse the result when the stylesheet hasn't changed:
import 'jest-styled-components/cache'That's itβthe cache automatically invalidates when the stylesheet changes (new components render) and when resetStyleSheet runs between tests via beforeEach. No manual cleanup needed.
With the cached entry point, both toHaveStyleRule and the snapshot serializer reuse the parsed stylesheet when possible. The serializer builds a filtered copy of the AST instead of mutating it during serialization.
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.
If styles are not appearing in snapshots, you may have multiple instances of styled-components loaded (common in monorepos). See the styled-components FAQ: Why am I getting a warning about several instances of module on the page?
Open an issue to discuss before submitting a PR.
This project exists thanks to all the people who contribute.
Thank you to all our backers! [Become a backer]
Support this project by becoming a sponsor. [Become a sponsor]
Licensed under the MIT License.