A blazingly fast, lightweight virtual DOM implementation powered by Bun. Drop-in replacement for happy-dom and jsdom in testing environments.
- Comprehensive DOM - Full DOM manipulation, CSS selectors, XPath, events with bubbling/capturing
- Network APIs - Fetch, XMLHttpRequest, WebSocket, and request interception
- Browser APIs - Storage, Timers, Canvas 2D, Observers, Clipboard, History, Cookies, File API
- Web Components - Custom Elements and Shadow DOM
- Framework Agnostic - Works with Bun, Vitest, or any testing framework
- Easy Migration - API-compatible with happy-dom; one-line switch from jsdom
bun add -d very-happy-domOr with npm/pnpm:
npm install --save-dev very-happy-dom
pnpm add -D very-happy-domimport { Window } from 'very-happy-dom'
const window = new Window()
const document = window.document
document.body.innerHTML = '<h1>Hello World</h1>'
const heading = document.querySelector('h1')
console.log(heading?.textContent) // "Hello World"The simplest way — create a Window per test:
import { describe, expect, test } from 'bun:test'
import { Window } from 'very-happy-dom'
describe('MyComponent', () => {
test('renders correctly', () => {
const window = new Window()
const document = window.document
document.body.innerHTML = '<div class="container">Test</div>'
const element = document.querySelector('.container')
expect(element?.textContent).toBe('Test')
})
})For Testing Library, React, and other frameworks that expect browser globals (document, window, etc.), use GlobalRegistrator:
// happy-dom.ts (preload script)
import { GlobalRegistrator } from 'very-happy-dom'
GlobalRegistrator.register()# bunfig.toml
[test]
preload = ["./happy-dom.ts"]That's it. All browser globals are now available in your tests:
import { test, expect } from 'bun:test'
import { screen, render } from '@testing-library/react'
import { MyComponent } from './MyComponent'
test('renders correctly', () => {
render(<MyComponent />)
expect(screen.getByTestId('my-component')).toBeInTheDocument()
})One-line change — the GlobalRegistrator API is the same:
-import { GlobalRegistrator } from '@happy-dom/global-registrator'
+import { GlobalRegistrator } from 'very-happy-dom'
GlobalRegistrator.register()import { Browser } from 'very-happy-dom'
const browser = new Browser()
const context = browser.createContext()
const page = context.newPage()
page.goto('https://example.com')import { Window } from 'very-happy-dom'
const window = new Window()
window.interceptor.addInterceptor({
onRequest: (request) => {
if (request.url.includes('/api/')) {
return new Response(JSON.stringify({ mocked: true }))
}
return request
}
})import { Window } from 'very-happy-dom'
const window = new Window({
url: 'https://example.com',
width: 1920,
height: 1080,
settings: {
navigator: {
userAgent: 'MyCustomUserAgent/1.0'
},
device: {
prefersColorScheme: 'dark'
}
}
})const window = new Window()
const document = window.document
const button = document.createElement('button')
let clicked = false
button.addEventListener('click', () => {
clicked = true
})
button.click()
console.log(clicked) // trueconst window = new Window()
// localStorage
window.localStorage.setItem('key', 'value')
console.log(window.localStorage.getItem('key')) // "value"
// sessionStorage
window.sessionStorage.setItem('session', 'data')const window = new Window()
const document = window.document
// MutationObserver
const observer = new window.MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log('DOM changed:', mutation.type)
})
})
observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true
})
// IntersectionObserver
const io = new window.IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log('Visibility changed:', entry.isIntersecting)
})
})
// ResizeObserver
const ro = new window.ResizeObserver((entries) => {
entries.forEach((entry) => {
console.log('Size changed:', entry.contentRect)
})
})const window = new Window()
const document = window.document
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 100, 100)
ctx.strokeStyle = 'blue'
ctx.strokeRect(10, 10, 80, 80)
// Export canvas data
const dataUrl = canvas.toDataURL()
const blob = await canvas.toBlob()| Operation | very-happy-dom | happy-dom | jsdom | Faster by |
|---|---|---|---|---|
| Window Creation | 4.08 µs | 92.83 µs | 1.22 ms | 22.7x |
| createElement | 463.02 ns | 2.62 µs | 4.67 µs | 5.7x |
| createElement + setAttribute | 748.35 ns | 15.41 µs | 6.62 µs | 8.8x |
| innerHTML (medium) | 41.61 µs | 47.48 µs | 168.98 µs | 1.1x |
| innerHTML (large, 200 nodes) | 1.92 ms | 3.72 ms | 6.27 ms | 1.9x |
| querySelector by ID | 81.03 ns | n/a | 2.76 µs | 34.1x |
| querySelector by class | 242.20 ns | n/a | 3.52 µs | 14.5x |
| querySelectorAll (200 matches) | 66.44 µs | n/a | 66.55 µs | ~1x |
| querySelectorAll + iteration | 76.44 µs | n/a | 170.37 µs | 2.2x |
| appendChild (single) | 1.70 µs | 4.58 µs | 6.14 µs | 2.7x |
| appendChild (1000 children) | 852.90 µs | 1.54 ms | 4.45 ms | 1.8x |
| setAttribute | 124.66 ns | 2.64 µs | 1.43 µs | 11.5x |
| getAttribute | 2.18 ns | 28.85 ns | 194.98 ns | 13.2x |
| classList.add | 3.97 µs | 6.88 µs | 4.87 µs | 1.2x |
| addEventListener + dispatch | 2.67 µs | 5.43 µs | 3.65 µs | 1.4x |
| textContent set | 470.48 ns | 1.72 µs | 4.67 µs | 3.7x |
| cloneNode (deep) | 6.16 µs | 21.59 µs | 15.55 µs | 2.5x |
| style.setProperty | 490.62 ns | 4.20 µs | 4.64 µs | 8.6x |
| Build data table (50x5) | 519.30 µs | 754.42 µs | 2.89 ms | 1.5x |
| Update list items (100) | 454.98 µs | n/a | 2.41 ms | 5.3x |
Note: "Faster by" compares very-happy-dom to the next-fastest result. Benchmarks run on Apple M3 Pro with Bun 1.3.11. Run them yourself:
bun run benchOne-line change — the API is compatible:
// Before
import { Window } from 'happy-dom'
// After
import { Window } from 'very-happy-dom'// Before (jsdom)
import { JSDOM } from 'jsdom'
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>')
const { window } = dom
const { document } = window
// After (very-happy-dom)
import { Window } from 'very-happy-dom'
const window = new Window()
const document = window.document- Window - Main window/global object with all browser APIs
- Document - DOM document with querySelector, createElement, etc.
- Element - DOM elements with full manipulation API
- Browser - Browser instance for advanced scenarios
- BrowserContext - Isolated browser contexts
- BrowserPage - Individual pages with navigation
Click to expand full API list
- Document, Element, TextNode, CommentNode, DocumentFragment
- Attributes, ClassList, Style
- querySelector / querySelectorAll
- getElementById / getElementsByClassName / getElementsByTagName
- CSS Selectors (all combinators)
- XPath
- addEventListener / removeEventListener
- Event bubbling and capturing
- CustomEvent
- Event.preventDefault / stopPropagation
- fetch(), Request / Response / Headers, FormData
- XMLHttpRequest
- WebSocket
- Request Interception
- localStorage, sessionStorage
- setTimeout / clearTimeout
- setInterval / clearInterval
- requestAnimationFrame / cancelAnimationFrame
- MutationObserver, IntersectionObserver, ResizeObserver
- Canvas element, 2D rendering context
- Basic drawing operations, toDataURL / toBlob
- Custom Elements, Shadow DOM
- Performance API, Console API, Clipboard API, Navigator API
- Geolocation API, Notification API, History API, Location API
- Cookie API, File API, FileReader API
bun test # Run all tests
bun test --coverage # Run with coverageWe welcome contributions! Please see CONTRIBUTING for details.
Please see our releases page for more information on what has changed recently.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
Very Happy DOM is free and open-source, but we'd love to receive a postcard from you! Send one to:
Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
We showcase postcards from around the world on our website!
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙
