How to snapshot test components in React

Snapshot testing in React captures component output and compares it against stored snapshots to catch unexpected UI changes. As the creator of CoreUI with over 12 years of React experience since 2014, I’ve used snapshot tests extensively to prevent UI regressions. Jest provides built-in snapshot testing that creates readable snapshots of React component trees and markup. This approach catches unintended changes in component structure, props, and rendered output without manual assertions.

Use Jest snapshot testing to capture React component output and detect unintended UI changes.

Basic component snapshot:

// Button.jsx
export default function Button({ variant = 'primary', children, onClick, disabled }) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  )
}

// Button.test.jsx
import { render } from '@testing-library/react'
import Button from './Button'

describe('Button', () => {
  it('renders primary button', () => {
    const { container } = render(<Button>Click me</Button>)
    expect(container.firstChild).toMatchSnapshot()
  })

  it('renders secondary button', () => {
    const { container } = render(<Button variant='secondary'>Secondary</Button>)
    expect(container.firstChild).toMatchSnapshot()
  })

  it('renders disabled button', () => {
    const { container } = render(<Button disabled>Disabled</Button>)
    expect(container.firstChild).toMatchSnapshot()
  })
})

// Creates: __snapshots__/Button.test.jsx.snap

Snapshot with props variations:

// Card.jsx
export default function Card({ title, subtitle, children, footer, variant = 'default' }) {
  return (
    <div className={`card card-${variant}`}>
      {(title || subtitle) && (
        <div className='card-header'>
          {title && <h3>{title}</h3>}
          {subtitle && <p>{subtitle}</p>}
        </div>
      )}
      <div className='card-body'>{children}</div>
      {footer && <div className='card-footer'>{footer}</div>}
    </div>
  )
}

// Card.test.jsx
import { render } from '@testing-library/react'
import Card from './Card'

describe('Card snapshots', () => {
  it('matches snapshot with minimal props', () => {
    const { container } = render(
      <Card>Content</Card>
    )
    expect(container.firstChild).toMatchSnapshot()
  })

  it('matches snapshot with all props', () => {
    const { container } = render(
      <Card
        title='Card Title'
        subtitle='Card Subtitle'
        variant='success'
        footer={<button>Action</button>}
      >
        Card content here
      </Card>
    )
    expect(container.firstChild).toMatchSnapshot()
  })

  it('matches snapshot for different variants', () => {
    const variants = ['default', 'primary', 'success', 'warning', 'danger']

    variants.forEach(variant => {
      const { container } = render(
        <Card variant={variant}>Content</Card>
      )
      expect(container.firstChild).toMatchSnapshot(`variant-${variant}`)
    })
  })
})

Inline snapshots:

// Alert.jsx
export default function Alert({ type = 'info', message }) {
  return (
    <div className={`alert alert-${type}`} role='alert'>
      {message}
    </div>
  )
}

// Alert.test.jsx
import { render } from '@testing-library/react'
import Alert from './Alert'

describe('Alert', () => {
  it('renders info alert', () => {
    const { container } = render(<Alert message='Info message' />)

    expect(container.firstChild).toMatchInlineSnapshot(`
      <div
        class="alert alert-info"
        role="alert"
      >
        Info message
      </div>
    `)
  })

  it('renders error alert', () => {
    const { container } = render(<Alert type='error' message='Error message' />)

    expect(container.firstChild).toMatchInlineSnapshot(`
      <div
        class="alert alert-error"
        role="alert"
      >
        Error message
      </div>
    `)
  })
})

Snapshot with dynamic data:

// UserCard.jsx
export default function UserCard({ user }) {
  return (
    <div className='user-card'>
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <span className='user-id'>ID: {user.id}</span>
      <time>{new Date(user.createdAt).toLocaleDateString()}</time>
    </div>
  )
}

// UserCard.test.jsx
import { render } from '@testing-library/react'
import UserCard from './UserCard'

describe('UserCard', () => {
  it('matches snapshot structure', () => {
    const mockUser = {
      id: Math.random(), // Dynamic
      name: 'John Doe',
      email: '[email protected]',
      avatar: 'https://example.com/avatar.jpg',
      createdAt: new Date().toISOString() // Dynamic
    }

    const { container } = render(<UserCard user={mockUser} />)

    // Use property matchers for dynamic values
    expect(container.firstChild).toMatchSnapshot({
      children: expect.arrayContaining([
        expect.objectContaining({ type: 'img' }),
        expect.objectContaining({ type: 'h3' }),
        expect.any(Object) // For dynamic date
      ])
    })
  })
})

Snapshot testing forms:

// LoginForm.jsx
export default function LoginForm({ onSubmit, error }) {
  return (
    <form onSubmit={onSubmit}>
      <div className='form-group'>
        <label htmlFor='email'>Email</label>
        <input id='email' type='email' required />
      </div>
      <div className='form-group'>
        <label htmlFor='password'>Password</label>
        <input id='password' type='password' required />
      </div>
      {error && <div className='error'>{error}</div>}
      <button type='submit'>Login</button>
    </form>
  )
}

// LoginForm.test.jsx
import { render } from '@testing-library/react'
import LoginForm from './LoginForm'

describe('LoginForm', () => {
  it('matches snapshot without error', () => {
    const { container } = render(<LoginForm onSubmit={jest.fn()} />)
    expect(container.firstChild).toMatchSnapshot()
  })

  it('matches snapshot with error', () => {
    const { container } = render(
      <LoginForm onSubmit={jest.fn()} error='Invalid credentials' />
    )
    expect(container.firstChild).toMatchSnapshot()
  })
})

Snapshot testing lists:

// TodoList.jsx
export default function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul className='todo-list'>
      {todos.map(todo => (
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <input
            type='checkbox'
            checked={todo.completed}
            onChange={() => onToggle(todo.id)}
          />
          <span>{todo.text}</span>
          <button onClick={() => onDelete(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}

// TodoList.test.jsx
import { render } from '@testing-library/react'
import TodoList from './TodoList'

describe('TodoList', () => {
  const mockTodos = [
    { id: 1, text: 'Task 1', completed: false },
    { id: 2, text: 'Task 2', completed: true },
    { id: 3, text: 'Task 3', completed: false }
  ]

  it('matches snapshot with todos', () => {
    const { container } = render(
      <TodoList
        todos={mockTodos}
        onToggle={jest.fn()}
        onDelete={jest.fn()}
      />
    )
    expect(container.firstChild).toMatchSnapshot()
  })

  it('matches snapshot with empty list', () => {
    const { container } = render(
      <TodoList todos={[]} onToggle={jest.fn()} onDelete={jest.fn()} />
    )
    expect(container.firstChild).toMatchSnapshot()
  })
})

Custom serializers:

// setupTests.js
import { cleanup } from '@testing-library/react'

afterEach(cleanup)

// Remove data-testid attributes from snapshots
expect.addSnapshotSerializer({
  test: (val) => val && val.hasAttribute && val.hasAttribute('data-testid'),
  print: (val, serialize) => {
    const clone = val.cloneNode(true)
    clone.removeAttribute('data-testid')
    return serialize(clone)
  }
})

// Component.test.jsx
it('snapshot without data-testid', () => {
  const { container } = render(
    <div data-testid='my-component'>Content</div>
  )
  expect(container).toMatchSnapshot() // data-testid removed
})

Snapshot with different states:

// Accordion.jsx
import { useState } from 'react'

export default function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div className='accordion'>
      <button onClick={() => setIsOpen(!isOpen)}>
        {title}
        <span>{isOpen ? '▼' : '▶'}</span>
      </button>
      {isOpen && <div className='content'>{children}</div>}
    </div>
  )
}

// Accordion.test.jsx
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Accordion from './Accordion'

describe('Accordion', () => {
  it('matches snapshot when closed', () => {
    const { container } = render(
      <Accordion title='Title'>Content</Accordion>
    )
    expect(container.firstChild).toMatchSnapshot()
  })

  it('matches snapshot when open', async () => {
    const user = userEvent.setup()
    const { container } = render(
      <Accordion title='Title'>Content</Accordion>
    )

    await user.click(container.querySelector('button'))

    expect(container.firstChild).toMatchSnapshot()
  })
})

Update snapshots:

# When component changes are intentional
npm test -- --updateSnapshot

# Or in watch mode
npm test -- --watch
# Press 'u' to update snapshots

Best practices:

describe('Snapshot best practices', () => {
  it('keeps snapshots small and focused', () => {
    // Test individual components, not entire pages
    const { container } = render(<Button>Click</Button>)
    expect(container.firstChild).toMatchSnapshot()
  })

  it('uses descriptive test names', () => {
    // Good: describes what's being tested
    const { container } = render(<Button variant='primary'>Save</Button>)
    expect(container.firstChild).toMatchSnapshot('primary-save-button')
  })

  it('combines with traditional assertions', () => {
    const { container, getByText } = render(<Button>Click</Button>)

    // Traditional assertion for critical behavior
    expect(getByText('Click')).toBeInTheDocument()

    // Snapshot for overall structure
    expect(container.firstChild).toMatchSnapshot()
  })
})

Best Practice Note

Keep snapshots small and focused on individual components—large snapshots are hard to review. Use inline snapshots for small components to keep tests readable. Name snapshots descriptively when testing multiple variations. Review snapshot diffs carefully during code review—they’re version-controlled. Update snapshots only when changes are intentional. Combine snapshots with traditional assertions for critical behavior. Use custom serializers to exclude dynamic or irrelevant attributes. This is how we use snapshot testing in CoreUI React components—catching UI regressions, ensuring consistent rendering across variations, and maintaining visual stability in production component libraries.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team