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"]Bun releases through 1.4.4 don't implement the snapshot serializer API, so toHaveStyleRule works and snapshots keep hashed class names with no CSS. Upgrade to a later release to get inlined CSS in snapshots.
toHaveStyleRule can't find createGlobalStyle rules under Bun on styled-components 6.6.0.
Add this setup only if your tests assert createGlobalStyle rules and you use styled-components v5, 6.0 through 6.2, or 6.4.0 and later. Jest and Vitest load the Node server build of styled-components, which does not inject these rules during a client render on those versions. The browser build does.
The setup has costs:
- Server rendering. The browser build throws when code renders in a Node environment with no
document, includingrenderToStringandServerStyleSheet.interleaveWithNodeStream. Run those tests in a separate Jest or Vitest project without this setup. - Snapshots. A global style that targets a component adds its rule to that component's snapshot and renumbers its class placeholders. Review and update those snapshots once.
Jest
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
moduleNameMapper: {
'^styled-components$': require.resolve(
'styled-components/dist/styled-components.browser.cjs.js'
),
},
}Resolution happens before any import, so a test file can import styled-components and this package in either order.
Vitest
// vitest.config.ts
import { createRequire } from 'node:module'
import { defineConfig } from 'vitest/config'
const require = createRequire(import.meta.url)
export default defineConfig({
resolve: {
alias: [
{
find: /^styled-components$/,
replacement: require.resolve(
'styled-components/dist/styled-components.browser.cjs.js'
),
},
],
},
test: {
environment: 'jsdom',
server: {
deps: {
inline: ['jest-styled-components'],
},
},
setupFiles: ['./setupTests.ts'],
},
})Install jsdom as a dev dependency. server.deps.inline makes Vite process the Vitest entry, so the entry follows the alias and the matcher reads the same sheet as the test. Use the alias, this setting, and the jest-styled-components/vitest import together. With the alias alone, or with the main jest-styled-components import, the matcher reads the server build's separate sheet: toHaveStyleRule reports that no rules were found, and snapshots keep hashed class names with no CSS.
The alias reaches only modules that Vite processes, which has a further cost under Vitest:
- Component libraries. A library in
node_modulesthat uses styled-components stays on the Node build, sotoHaveStyleRulefinds no rules on its components. Add an ES module library toserver.deps.inline. A CommonJS library can't follow the alias, so a project that asserts on its components can't use this setup.
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.
Depending on your styled-components version, this assertion needs the browser build.
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)With the Vitest alias from the browser build setup, import jest-styled-components/vitest as well. This export can't follow the alias by itself, so snapshots keep hashed class names with no CSS.
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).
- Browser build setup. A
createGlobalStylerule can need the browser build. Under Vitest, an incomplete setup hides every rule, and components from a library innode_modulesneed extra handling. See Browser build forcreateGlobalStyle.
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). An incomplete Vitest browser build setup causes this too.
Global styles don't attach to a specific component. Use the selector option to match by CSS selector. Depending on your styled-components version, this needs the browser build:
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.