Skip to main content
Latest on AP
March 5, 2026Analysis

Google Antigravity: The Complete Developer Guide (2026)

Google Antigravity applies real-time JavaScript physics to the Google homepage — elements float, collide, and drift. Complete 2026 guide: 11 verified fixes, six-layer architecture, Matter.js implementation, performance hacks, and security warnings.

March 5, 2026
Google Antigravity 2026Google Gravity JavaScript fixGoogle Antigravity not workingMatter.js DOM physics tutorialbrowser physics experiment 2026Google Gravity mobile fixCSS transform GPU animationBox2D JavaScript browserelgoog.im mirror safe 2026Google Antigravity lag fixDOM physics implementation guideGoogle Antigravity vs AI IDE disambiguation
Google Antigravity: The Complete Developer Guide (2026)

In 2009, Ricardo Cabello — known as Mr. Doob — submitted a Chrome Experiment proving something no one had properly demonstrated: a browser could run a real-time rigid-body physics simulation without plugins, servers, or any infrastructure at all. Every element of the Google homepage fell, bounced, and collided using the same physics engine behind Angry Birds.

Google featured it in the Chrome Blog in 2012. It went globally viral. Dozens of regional forks appeared within months.

Seventeen years later, the experiment still runs. It still breaks in precisely the same ways it always has — and developers still debug it without understanding why it breaks. In March 2026, two new complications have entered the picture.

The first: the experiment's name now belongs to two products simultaneously. In November 2025, Google launched "Antigravity" — a fully separate agentic AI IDE built on VS Code with Gemini 3. Developers searching for one product now regularly land on documentation for the other, creating support dead ends that no article has resolved clearly until now.

The second: the fake mirror site ecosystem has become a genuine security hazard. Malicious actors have been creating Google Gravity clones that bundle the legitimate physics effect with credential stealers, crypto miners, and drive-by malware — and no mainstream coverage of the experiment warns about it.

This guide resolves both. For casual users: every reason Google Antigravity is broken in your browser in 2026, and exactly how to fix it. For developers: the complete six-layer architecture, the performance patterns separating a smooth 60fps experience from a CPU-pinning freeze, and a drop-in Matter.js implementation you can apply to any webpage today.

The Naming Problem: One Name, Two Products — A 2026 Disambiguation

This disambiguation must come first. In March 2026, searching "Google Antigravity" returns results for two completely unrelated products:

DimensionGoogle Anti-Gravity (Physics Experiment)Google Antigravity (AI IDE)
OriginRicardo Cabello (Mr. Doob), 2009Google, November 2025
What it isClient-side DOM physics browser experimentAgentic AI coding platform, VS Code + Gemini 3
Where it runselgoog.im, mrdoob.com mirrorsantigravity.google
Official product?No — third-party experimentYes — official Google developer product
TechnologyBox2DJS / Matter.js physics engineGemini 3, VS Code fork, AI coding agent
Server dependencyNone — fully client-sideCloud-hosted AI inference
Who uses itCasual users, browser experiment fansSoftware developers, engineers

If you are debugging the AI coding IDE — stop here. This article is exclusively about the 2009 browser physics experiment. The AI IDE has no connection to this codebase, this physics engine, or these mirror sites.

How Google Antigravity Works: The Complete Six-Layer Architecture

Understanding the full pipeline is the prerequisite for both effective debugging and building your own version. Every failure mode documented later in this article traces back to one specific layer.

🏗️ Architecture

The Six-Layer DOM Physics Pipeline

Every failure mode in Google Antigravity traces back to one of these six layers — understanding the pipeline is the foundation for all debugging.

📄
L01
HTML Snapshot

Self-contained HTML file with base64-inlined assets

elgoog.im, mrdoob.com serve a frozen replica of Google's UI

⚠️ Live google.com blocks scripts via CSP — only static mirrors work
📐
L02
DOM Position Capture

getBoundingClientRect() called on all target elements in one batch

All reads MUST be batched together — interleaved reads/writes cause reflow

⚠️ Must capture positions before any CSS changes to prevent stale data
⚙️
L03
Physics World Init

Box2D or Matter.js world created with gravity + boundary walls

gravity.y = -1 (up), +1 (down), 0 (zero). Walls at viewport edges.

⚠️ A single gravity vector value determines the entire visual behavior
✂️
L04
DOM Detachment

position: fixed, left: 0, top: 0, transform for placement

After this step the layout engine no longer controls these elements

⚠️ Elements must leave normal document flow to be positioned by physics
🔄
L05
Render Loop

requestAnimationFrame → Engine.update() → write transform to DOM

Center-to-corner offset correction: subtract half-width/height from physics x/y

⚠️ Physics position → CSS transform must happen 60× per second
🖱️
L06
Input Handling

Pointer events → mouse joint (spring constraint) → throw on release

Use Pointer Events API (not mouse events) for mobile compatibility

⚠️ Mouse/touch interaction must become physics forces, not DOM events

Debugging rule: When the effect breaks, identify which layer failed first. 90% of issues are at Layer 1 (CSP), Layer 2 (race condition), or Layer 5 (transform vs top/left).

Layer 1 — HTML Snapshot (Why Mirror Sites Exist)

Mirror sites (elgoog.im, mrdoob.com) serve a self-contained static HTML file designed to look like the Google homepage. All assets are inlined or base64-encoded within the file.

This architecture is not a workaround — it is a necessity. Live Google.com sends strict Content Security Policy headers blocking any script not explicitly whitelisted in its CSP policy. Injecting the physics script against live google.com will always be blocked. The experiment can only run against a static snapshot on a domain with permissive CSP headers.

This is why "Google Antigravity not working" is almost never a browser bug. It is CSP policy by design.

Layer 2 — DOM Traversal and Position Capture

At initialization, the physics script queries all major page elements using CSS selectors. It immediately captures each element's position using getBoundingClientRect() — returning a DOMRect with x, y, width, and height relative to the viewport. This position snapshot must happen before any CSS changes are applied, and all reads must be batched together to prevent premature layout reflows.

Layer 3 — Physics World Initialization

A physics world is created using Box2DJS (original version) or Matter.js (modern versions). Gravity direction and magnitude are configured here — this is the single value distinguishing Anti-Gravity (negative Y), standard Gravity (positive Y), and Zero Gravity (zero). Invisible static boundary bodies are created at all four viewport edges: floor, ceiling, left wall, right wall.

Layer 4 — DOM Detachment

Each element is removed from normal document flow by setting it to position: fixed anchored at a zero origin, with transform handling all positioning. Parent container positioning is flattened to prevent stacking context interference. From this point, the element's visual position is owned entirely by the physics engine — not the browser's layout system.

Layer 5 — The Render Loop

requestAnimationFrame drives a loop at up to 60fps. Each frame executes exactly two operations: step the physics world forward by one timestep (updating all body positions and rotation angles), then read each body's new position and write it to the corresponding DOM element's CSS transform property. This two-step pattern is the complete mechanism.

Layer 6 — Input Handling

Mouse and pointer events are translated into physics forces. On pointerdown, the element under the cursor is identified and its physics body is attached to a spring constraint between the body and cursor position — a "mouse joint." Moving the cursor drags the body via spring force. On pointerup, the spring releases and the body's current velocity carries it forward.

The Gravity Variants: One Architecture, Four Behaviors

All four major variants share the same six-layer architecture. The entire behavioral difference is a single vector value in Layer 3:

🌍 Variant Guide

The Four Gravity Variants — One Architecture, Four Behaviors

All four share identical code. The only difference is a single vector value in the physics engine.

⬆️
Google Anti-Gravity
Original by Mr. Doob

Elements float upward toward the screen top, drift and collide at the ceiling.

engine.gravity.y: -1
Logo drifts upSearch bar hits ceilingMouse drag launches elements
⬇️
Google Gravity
Most viral variant

All elements collapse to the viewport floor, pile up in a heap, and respond to being thrown.

engine.gravity.y: +1
Logo smashes floorElements stack and pileThrow them: they fly
🌌
Google Space
Zero gravity

Zero gravity — elements drift weightlessly. Cursor emits a repulsion field pushing them away.

engine.gravity.x: 0, y: 0
Elements drift freelyCursor repels everythingMouse magnet mode
🌋
Google Gravity Lava
Visual variant

Elements fall downward, but on collision they visually "melt" — special shader applied on impact.

engine.gravity.y: +1
Elements fall normallyCollision = melt effectPool of lava forms

The key insight: Change engine.gravity.y from -1 to +1 and the entire visual experience inverts.

In Matter.js, activating anti-gravity requires changing one line: engine.gravity.y = -1. Every other architectural element remains identical.

11 Hacks: Root Causes and Verified Fixes

Hack 1 — Script Does Nothing: Page Loads Normally, No Physics

Root Cause: JavaScript is disabled, or a script-blocking extension is silently preventing execution. The entire effect is JavaScript-driven from initialization to render loop. Without it, the page loads without error and without physics — and no visible indication is shown.

Fix — ordered by likelihood:

Step 1: Test in private/incognito mode first. Extensions are disabled by default in incognito. If the physics works there, an extension in your normal session is blocking it.

Step 2: Verify JavaScript is enabled in your browser:
Chrome: Settings → Privacy and Security → Site Settings → JavaScript → Allow
Firefox: about:config → javascript.enabled → must be true
Safari: Preferences → Security → Enable JavaScript

Step 3: Open DevTools (F12) → Console tab. Red text reading "Refused to execute script" or any CSP-related message means you are attempting to run the script against live google.com. This will always fail. Use a mirror site.

Step 4: Disable extensions one at a time to identify the blocker. Primary suspects: uBlock Origin (hard mode), NoScript, Privacy Badger, Ghostery. These tools frequently flag the Box2D or Matter.js libraries as third-party trackers and drop them silently.

Hack 2 — No Effect on Mobile: Broken or Garbled Layout

Root Cause: The original mrdoob.com version was built exclusively for desktop in 2009. It uses mousedown, mousemove, and mouseup events with zero touch event fallbacks. The physics coordinate math is hardcoded around fixed-pixel desktop viewport assumptions. Mobile viewports break the initial body placement math, and touch interactions do not register at all.

Fix:

Step 1: Do not use the original mrdoob.com version on mobile. The source has never been updated for touch support. It will always be broken on mobile.

Step 2: Use elgoog.im/gravity/ directly. This mirror has been rebuilt using the Pointer Events API, which handles mouse, touch, and stylus input uniformly with a single event model.

Step 3: For custom implementations, replace all mouse listeners with Pointer Events:

// Mouse events only — broken on mobile:
element.addEventListener('mousedown', handler);
element.addEventListener('mousemove', handler);
element.addEventListener('mouseup', handler);

// Pointer Events — handles mouse, touch, and stylus uniformly:
element.addEventListener('pointerdown', handler);
element.addEventListener('pointermove', handler);
element.addEventListener('pointerup', handler);

Step 4: For iOS Safari specifically, add { passive: true } to all pointer listeners to prevent scroll-blocking warnings that can prevent events from firing:

element.addEventListener('pointermove', handler, { passive: true });

Hack 3 — Extreme Lag, Browser Freeze, Fan Spinning Immediately

Root Cause: This is the most technically significant failure mode across all older forks and custom implementations. The physics engine updates element positions 60 times per second. Older implementations write to element.style.left and element.style.top. These two CSS properties trigger a full layout recalculation — the browser must recompute the geometry of every element on the page per frame. This is called layout thrashing.

When getBoundingClientRect() reads are interleaved with style writes within the same frame, the browser is forced to flush its pending layout queue repeatedly, multiplying the per-frame cost further.

What old forks do wrong:

// LAYOUT THRASHING — triggers full reflow on every element, every frame:
elements.forEach(el => {
    el.style.left = body.position.x + 'px';  // triggers reflow
    el.style.top  = body.position.y + 'px';  // triggers reflow again
});

The correct approach — GPU compositor, zero layout cost:

// USING CSS TRANSFORMS — bypasses layout entirely
elements.forEach(el => {
    el.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
});

CSS transform bypasses the layout engine entirely. The GPU compositor thread handles it independently of the main thread. Measured performance difference: 5–10× faster on pages with many elements.

Step 1: Verify hardware acceleration is enabled. In Chrome: navigate to chrome://gpu → confirm "Hardware accelerated" appears next to Canvas 2D, WebGL, and Compositing. If showing "Software only": Settings → System → Use hardware acceleration → toggle on → relaunch browser.

Step 2: Use elgoog.im. It uses the correct transform approach. Most third-party forks from 2012–2018 still use top/left and will always perform poorly.

Hack 4 — Search Bar Produces No Results

Root Cause: The original physics script connected to Google's Web Search API to fetch and display real search results. Google permanently deprecated and shut down this API in 2014. The physics simulation itself is completely functional — only the data pipeline is broken. Mirror sites attempt API emulation but may fail due to CORS restrictions, outdated credentials, or rate limiting.

Fix:

Step 1: Use elgoog.im — it maintains the most current search emulation layer.

Step 2: For custom implementations, the search bar is a fully functional HTML <input> element. Redirect form submissions to Google Search directly:

document.querySelector('form').addEventListener('submit', e => {
    e.preventDefault();
    const q = document.querySelector('input[name=q]').value;
    window.open('https://www.google.com/search?q=' + encodeURIComponent(q));
});

Hack 5 — Elements Start in Wrong Positions or Partially Off-Screen

Root Cause: A race condition between physics initialization and the page completing its final layout render. The script captures element positions using getBoundingClientRect() at startup. If web fonts, lazy-loading images, or async stylesheets have not finished loading at that exact moment, the captured positions are stale or zero. Physics bodies are then placed at incorrect coordinates that do not match the visual element positions.

Fix:

Step 1: Switch initialization from DOMContentLoaded to window load:

// Wrong — fires before fonts and images have loaded:
document.addEventListener('DOMContentLoaded', initPhysics);

// Correct — fires only after the complete page render:
window.addEventListener('load', initPhysics);

Step 2: If web fonts specifically are the cause, use document.fonts.ready:

document.fonts.ready.then(() => initPhysics());

Step 3: As a hard fallback — add a 300–500ms delay after the load event to allow all reflows from late-loading resources to fully settle before capturing positions.

Hack 6 — CSP Blocks Script Execution Against Live Google.com

Root Cause: Modern Google.com sends strict Content Security Policy headers that block any script not explicitly whitelisted. The browser console shows: "Refused to execute inline script because it violates the following Content Security Policy directive." This is architecture — not a bug.

Fix:

Step 1: Do not attempt to run physics scripts against live google.com. It is not technically possible without a browser extension overriding CSP headers. No amount of configuration will change this.

Step 2: Use mirror sites (elgoog.im) that host their own static Google UI snapshot with permissive or absent CSP headers on their own domain.

Hack 7 — Elements Escape Through Walls After Browser Resize

Root Cause: The invisible boundary walls (floor, ceiling, left wall, right wall) are static physics bodies created once at initialization using the viewport dimensions at that exact moment. When the browser window is resized, the visual boundary moves — but the physics bodies remain at their original coordinates. Elements then pass through the gap between the old physics wall position and the actual new screen edge.

Fix:

let walls = createBoundaryWalls();

window.addEventListener('resize', () => {
    // Must explicitly remove old walls BEFORE adding new ones.
    // Adding new walls without removing old creates invisible duplicate
    // collision surfaces inside the viewport — elements bounce off nothing.
    walls.forEach(wall => World.remove(engine.world, wall));
    
    // recreate at new viewport dimensions
    walls = createBoundaryWalls();   
    World.add(engine.world, walls);
});

Hack 8 — Elements Tunnel Through Walls at High Speed

Root Cause: Classic discrete physics simulation tunneling. The engine steps forward in 1/60th-second increments per frame. If an element moves farther than its own width or height within a single step, collision detection checks positions before and after the step — but never detects the overlap that existed mid-step. The element passes straight through the wall.

Fix — Box2D approach: Enable Continuous Collision Detection on fast-moving bodies:

body.SetBullet(true);

Fix — Matter.js approach: Cap maximum velocity in the beforeUpdate event:

Events.on(engine, 'beforeUpdate', () => {
    const maxVel = 20;
    Composite.allBodies(engine.world).forEach(body => {
        if (body.speed > maxVel) Body.setSpeed(body, maxVel);
    });
});

Hack 9 — iframe Embedding: Mouse Coordinates Are Offset

Root Cause: Inside an iframe, event.clientX and event.clientY are relative to the iframe's own viewport origin — not the parent page. If the iframe is embedded at an offset position within the parent page, dragging physics elements appears to lag behind the cursor by exactly that offset distance.

Fix:

// In the parent page — translate coordinates to iframe-relative:
document.addEventListener('mousemove', (e) => {
    const iframe = document.querySelector('iframe#gravity-frame');
    const rect   = iframe.getBoundingClientRect();
    iframe.contentWindow.postMessage({
        type: 'MOUSE_MOVE',
        x: e.clientX - rect.left,
        y: e.clientY - rect.top
    }, '*');
});

// Inside the iframe — receive translated coordinates:
window.addEventListener('message', (e) => {
    if (e.data.type === 'MOUSE_MOVE') {
        mouse.position.x = e.data.x;
        mouse.position.y = e.data.y;
    }
});

Hack 10 — Browser Extension Interference: Partial Elements Missing

Root Cause: Ad blockers match DOM elements against known ad unit signatures by size, class name, or position. Some Google homepage elements match these signatures and are silently removed from the DOM before the physics script runs. The script attempts to create physics bodies for missing elements, encounters null references, throws a JavaScript error, and in many implementations aborts the entire physics initialization.

Fix:

Step 1: Test in incognito mode to confirm whether extensions are responsible.

Step 2: Always null-check and filter removed elements before body creation:

const elements = [...document.querySelectorAll('.physics-target')]
    .filter(el => el !== null && el.offsetParent !== null);

Step 3: Wrap each body creation in try/catch to prevent a single missing element from aborting the full initialization:

elements.forEach(el => {
    try {
        const body = createPhysicsBody(el);
        World.add(engine.world, body);
    } catch (e) {
        console.warn('Skipped element:', el, e.message);
    }
});

Hack 11 — HTTPS Mixed-Content Warning Blocks Physics Library

Root Cause: Older forks reference Box2D.js and other libraries via http:// CDN URLs. Modern browsers block mixed active content — any script loaded over HTTP on an HTTPS page is silently blocked. The physics library fails to load, the global Box2D or Matter object is undefined, and the initialization script crashes immediately with a ReferenceError.

Fix:

<!-- INCORRECT - Loading HTTP on an HTTPS page will fail: -->
<script src="http://cdnjs.cloudflare.com/..."></script>

<!-- CORRECT - Using HTTPS along with SRI hash checks: -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js" 
        integrity="...hash..." 
        crossorigin="anonymous"></script>

The permanent solution is to self-host all physics libraries. This eliminates CDN dependency, mixed-content risk, Subresource Integrity maintenance, and third-party tracking simultaneously.

Physics Engine Deep Dive: Box2D vs Matter.js

Box2DJS (Original 2009 Engine)

Box2DJS is a direct JavaScript port of Erin Catto's Box2D C++ library — the same engine behind Angry Birds. It uses AABB (Axis-Aligned Bounding Box) broad-phase collision detection and an iterative constraint solver.

Critical developer notes that trip up most implementations:

Shapes use extents (half-width, half-height from center), not full dimensions. A 200×50px element requires extents.Set(100, 25). Developers expecting full-dimension APIs will offset every physics body.

Pixel coordinates must be divided by a world scale factor (typically 30) before passing to Box2D, then multiplied back by 30 for rendering. Box2D is calibrated for real-world meter units:

var gravity = new b2Vec2(0, -300);          // negative Y = upward anti-gravity
var world   = new b2World(worldAABB, gravity, true);

var bodyDef  = new b2BodyDef();
bodyDef.position.Set(el_x / 30, el_y / 30); // divide all coords by scale

var shapeDef       = new b2BoxDef();
shapeDef.extents.Set(width / 2 / 30, height / 2 / 30); // half-extents, scaled
shapeDef.density   = 1.0;
shapeDef.friction  = 0.3;
shapeDef.restitution = 0.2;  // 0 = no bounce | 1 = perfect elastic bounce
bodyDef.AddShape(shapeDef);

var body = world.CreateBody(bodyDef);

Matter.js (2026 Standard)

Matter.js is the correct choice for any new DOM physics implementation in 2026. It works natively in pixel coordinates, provides a significantly cleaner API, and performs better at high body counts through optimized broad-phase spatial hashing.

const engine = Engine.create({
    gravity: { x: 0, y: -1 }  // y: -1 = anti-gravity  |  y: 0 = zero  |  y: 1 = normal
});

const rect = element.getBoundingClientRect();
const body = Bodies.rectangle(
    rect.left + rect.width  / 2,  // center X
    rect.top  + rect.height / 2,  // center Y
    rect.width,
    rect.height,
    {
        restitution: 0.3,   // bounciness
        friction:    0.1,   // surface friction
        frictionAir: 0.02,  // air resistance — controls how "floaty" anti-gravity feels
        density:     0.002  // mass relative to area
    }
);

Physics Material Reference — All Five Presets

🧪 Physics Materials

Interactive Material Reference

Click a material to explore its properties and the exact Matter.js values that create that feel.

🔴
Rubber
Bouncy, grippy

High elasticity, sticks to surfaces, bounces vigorously on impact.

Restitution (bounciness)0.90
Friction0.80
Air Resistance0.10
Density0.40
MATTER.JS
Bodies.rectangle(x, y, w, h, {
  restitution: 0.9,
  friction:    0.8,
  frictionAir: 0.01,
  density:     0.002
});

frictionAir is the most impactful value for the anti-gravity variant — higher values cause elements to drift gently to rest rather than floating indefinitely.

Professional Workflow System: The PHYSICS Method

A proprietary six-phase implementation framework for deploying DOM physics on any webpage from zero to production-ready.

PHYSICS: Prepare → Hook → Initialize → Simulate → Control → Secure

🎯 Implementation Framework

The PHYSICS Method

PHYSIC

Prepare → Hook → Initialize → Simulate → Control → Secure — a complete deployment framework for DOM physics on any webpage.

📐
P
Prepare: Position Capture Without Thrashing

Call getBoundingClientRect() on every element first. Then do all style writes. Never interleave.

✗ Wrong
el.getBoundingClientRect() then el.style.position = "fixed" — repeated per element
✓ Correct
const rects = elements.map(el => el.getBoundingClientRect()); // then all writes
🔗
H
Hook: Boundary Wall Construction

Create invisible static bodies at viewport edges. Rebuild them on resize — old walls must be removed or duplicates accumulate.

✗ Wrong
walls.push(Bodies.rectangle(...)) on resize — old walls remain
✓ Correct
walls.forEach(w => World.remove(world, w)); walls = createBoundaryWalls();
⚙️
Y
Initialize: Physics Bodies from DOM Elements

Skip elements where offsetParent is null (hidden by extensions or CSS). Skip elements smaller than 4px. Wrap in try/catch.

✗ Wrong
elements.forEach(el => createPhysicsBody(el)) — crashes on hidden elements
✓ Correct
.filter(el => el.offsetParent !== null && r.width > 4 && r.height > 4)
🔄
S
Simulate: The Render Loop

Physics tracks body center. CSS positions from top-left. Subtract halfW/halfH or every element is offset by exactly half its size.

✗ Wrong
`translate(${body.position.x}px, ${body.position.y}px)` — visual offset bug
✓ Correct
`translate(${x - halfW}px, ${y - halfH}px) rotate(${angle}rad)`
🎮
I
Control: Runtime Gravity Switching

engine.gravity.y and engine.gravity.x are mutable live values. DeviceMotion tilt, rotating gravity, and keyboard gravity all modify these directly.

✗ Wrong
Recreating the physics world to change gravity direction
✓ Correct
engine.gravity.y = -1; // takes effect on next physics step
🔒
C
Secure: Production Safety Checklist

Check window.matchMedia("(prefers-reduced-motion: reduce)") before any init. Provide a keyboard-accessible reset button. Monitor FPS and degrade gracefully.

✗ Wrong
Initializing physics unconditionally on all devices
✓ Correct
if (!reduce.matches) initPhysics(); // skip for reduced-motion users

Rule of 60: Every phase has exactly one critical rule. Violating any one of the six rules produces a bug that no amount of debugging the other phases will fix.

Phase 1: Prepare — Position Capture Without Layout Thrashing

The single most important implementation decision is capturing all element positions before making any DOM changes. The wrong pattern — interleaving reads and writes — forces the browser to flush its layout queue per element:

// WRONG — read-write interleaving causes layout thrashing:
elements.forEach(el => {
    const r = el.getBoundingClientRect(); // READ — forces layout flush
    el.style.position = 'fixed';          // WRITE — invalidates layout
    el.style.left = r.left + 'px';        // cycle repeats per element
});

// CORRECT — batch ALL reads first, then ALL writes:
const rects = elements.map(el => el.getBoundingClientRect()); // all reads first

elements.forEach((el, i) => {
    const r = rects[i];
    el.style.position   = 'fixed';
    el.style.left       = '0';
    el.style.top        = '0';
    el.style.width      = r.width  + 'px';
    el.style.height     = r.height + 'px';
    el.style.transform  = `translate(${r.left}px, ${r.top}px)`;
    el.style.willChange = 'transform';
    el.style.margin     = '0';
    el.style.transition = 'none'; // remove CSS transitions — causes visual lag
});

Setting left: 0 and top: 0 then using transform for position is deliberate. It anchors each element at a fixed origin and delegates all actual positioning to the GPU compositor — the same pattern used by GSAP and all high-performance animation libraries.

Phase 2: Hook — Boundary Wall Construction With Resize Safety

function createBoundaryWalls(world) {
    const W = window.innerWidth;
    const H = window.innerHeight;
    const T = 60; // wall thickness in pixels — thicker prevents tunneling

    const walls = [
        Bodies.rectangle(W/2,   H+T/2, W+T*2, T, { isStatic: true }), // floor
        Bodies.rectangle(W/2,  -T/2,   W+T*2, T, { isStatic: true }), // ceiling
        Bodies.rectangle(-T/2,  H/2,   T, H+T*2, { isStatic: true }), // left wall
        Bodies.rectangle(W+T/2, H/2,   T, H+T*2, { isStatic: true }), // right wall
    ];

    World.add(world, walls);

    // Rebuild walls on resize — prevents element escape-through-walls bug
    window.addEventListener('resize', () => {
        walls.forEach(w => World.remove(world, w));
        createBoundaryWalls(world);
    });
}

Phase 3: Initialize — Physics Bodies from DOM Elements

function initPhysicsElements(engine) {
    const selector = 'img, h1, h2, h3, p, button, a, input, nav, header';
    const elements = [...document.querySelectorAll(selector)]
        .filter(el => !el.closest('[data-no-physics]')) // respect opt-out attribute
        .filter(el => el.offsetParent !== null);         // skip hidden or removed elements

    const rects  = elements.map(el => el.getBoundingClientRect()); // batch all reads
    const bodies = [];

    elements.forEach((el, i) => {
        const r = rects[i];
        if (r.width < 4 || r.height < 4) return; // skip invisible elements

        // Detach from document flow
        el.style.position   = 'fixed';
        el.style.left       = '0';
        el.style.top        = '0';
        el.style.width      = r.width  + 'px';
        el.style.height     = r.height + 'px';
        el.style.transform  = `translate(${r.left}px, ${r.top}px)`;
        el.style.willChange = 'transform';
        el.style.margin     = '0';
        el.style.transition = 'none';

        const body = Bodies.rectangle(
            r.left + r.width  / 2,
            r.top  + r.height / 2,
            r.width, r.height,
            { restitution: 0.35, friction: 0.1, frictionAir: 0.015 }
        );

        body._el    = el;
        body._halfW = r.width  / 2; // store half-dims for center-to-corner offset
        body._halfH = r.height / 2;
        bodies.push(body);
    });

    World.add(engine.world, bodies);
    return bodies;
}

Phase 4: Simulate — The Render Loop With Center-to-Corner Correction

The center-to-corner physics offset is the most frequently missed implementation detail. Physics engines track body position by center point. CSS positions elements from their top-left corner. Without subtracting half-dimensions, every element appears visually offset by exactly half its size in both dimensions:

function startRenderLoop(engine, bodies) {
    let running = true;
    let lastTimestamp = 0;

    // Pause physics when tab is hidden — saves CPU and battery
    document.addEventListener('visibilitychange', () => {
        running = !document.hidden;
    });

    function loop(timestamp) {
        if (running) {
            Engine.update(engine, 1000 / 60); // advance physics by 1/60th second

            bodies.forEach(body => {
                if (!body._el) return;
                const { x, y } = body.position;

                // CRITICAL: subtract half-dimensions to convert
                // center-origin (physics) → top-left-origin (CSS)
                body._el.style.transform =
                    `translate(${x - body._halfW}px, ${y - body._halfH}px) ` +
                    `rotate(${body.angle}rad)`;
            });
        }
        lastTimestamp = timestamp;
        requestAnimationFrame(loop);
    }

    requestAnimationFrame(loop);
}

Phase 5: Control — Runtime Gravity Switching and DeviceMotion

Runtime gravity direction — no world reset required:

const gravityModes = {
    normal:      { x:  0, y:  1   },
    antigravity: { x:  0, y: -1   },
    zero:        { x:  0, y:  0   },
    left:        { x: -1, y:  0   },
    right:       { x:  1, y:  0   },
};

function setGravity(mode) {
    engine.gravity.x = gravityModes[mode].x;
    engine.gravity.y = gravityModes[mode].y;
    // Takes effect on next physics step — no reset required
}

Continuously rotating gravity — perpetual tumbling wheel effect:

let angle = 0;
function rotatingGravityLoop() {
    angle += 0.01;
    engine.gravity.x = Math.sin(angle);
    engine.gravity.y = Math.cos(angle);
    requestAnimationFrame(rotatingGravityLoop);
}

DeviceMotion tilt control — physical device tilt becomes gravity direction (iOS 13+ requires permission via explicit user gesture):

window.addEventListener('devicemotion', (event) => {
    const acc = event.accelerationIncludingGravity;
    if (acc) {
        engine.gravity.x =  acc.x / 9.8;  // normalize device acceleration to -1..1
        engine.gravity.y = -acc.y / 9.8;  // Y-axis is inverted device vs. screen
    }
});

Phase 6: Secure — Production Safety and Accessibility Checklist

1. Check prefers-reduced-motion before any initialization:

const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
if (!reduce.matches) initPhysics();

Users with vestibular disorders, epilepsy, or motion sensitivity can experience genuine physical discomfort from animated physics simulations. This check is non-negotiable for any production deployment.

2. Opt-out attribute for navigation and critical elements:

<nav data-no-physics="true">This element stays fixed — not affected by physics</nav>

Never apply physics to navigation, form elements, error messages, or primary calls-to-action.

3. Complete physics reset for accessibility:

const originalStyles = new Map();
elements.forEach(el => originalStyles.set(el, el.getAttribute('style') || ''));

function resetPhysics() {
    running = false;
    World.clear(engine.world);
    Engine.clear(engine);
    originalStyles.forEach((style, el) => el.setAttribute('style', style));
}

Always provide a keyboard-accessible reset button that remains focusable and visible even while elements are floating.

4. FPS monitoring with graceful degradation:

let lowFpsCount = 0;
function loop(timestamp) {
    const fps = 1000 / (timestamp - lastTimestamp);
    if (fps < 15) {
        if (++lowFpsCount > 60) { resetPhysics(); return; }
    } else { lowFpsCount = 0; }
}

5. Subresource Integrity for CDN dependencies: The definitive solution: self-host all physics libraries. Eliminates CDN dependency, mixed-content risk, and SRI maintenance in one step.

Performance Optimization: Ranked by Verified Impact

⚡ Performance

10 Optimizations — Ranked by Verified Impact

Apply these in order. The top 3 alone remove 90% of performance problems in any DOM physics implementation.

🚀#01
Use transform, never top/leftCriticalGPU

5–10× faster. CSS transform runs on GPU compositor thread — zero layout cost.

el.style.transform = `translate(${x}px, ${y}px)`
📦#02
Batch all reads before writesHighReflow

Collect ALL getBoundingClientRect() calls first, then do all style writes. Prevents layout thrashing.

const rects = elements.map(el => el.getBoundingClientRect()); // then writes
🎨#03
Add will-change: transformHighGPU

Tell the browser in advance which elements will animate — promotes them to their own GPU layer.

el.style.willChange = 'transform'; // set during init
😴#04
Enable body sleepingHighPhysics

Resting bodies are excluded from physics simulation entirely — massive savings for settled scenes.

Engine.create({ enableSleeping: true })
🔇#05
Pause when tab is hiddenHighCPU/Battery

Stops simulation loop completely when user switches tabs. Saves CPU and battery.

document.addEventListener('visibilitychange', () => { running = !document.hidden; })
🔍#06
Filter invisible elementsMediumInit

Skip elements smaller than 4px or with offsetParent === null before creating physics bodies.

.filter(el => r.width > 4 && r.height > 4 && el.offsetParent !== null)
🚦#07
Cap maximum velocityMediumStability

Prevents tunneling through walls and reduces collision computation at high speeds.

Events.on(engine, 'beforeUpdate', () => { if (body.speed > 20) Body.setSpeed(body, 20); })
📱#08
Reduce to 30fps on mobileMediumMobile

Step physics every other requestAnimationFrame frame on mobile — halves CPU/battery cost.

let tick = 0; if (++tick % 2 === 0) Engine.update(engine, 1000/30);
🔢#09
Hard cap body countMediumScale

Cap at 80 physics bodies. Beyond that, quadratic collision detection cost dominates.

const elements = [...querySelectorAll(sel)].slice(0, 80);
#10
Respect prefers-reduced-motionA11yAccessibility

Non-negotiable for production. Users with vestibular disorders can experience physical harm from physics animations.

const reduce = matchMedia('(prefers-reduced-motion: reduce)'); if (!reduce.matches) initPhysics();

The single most impactful change anyone can make: Replace style.top/left with style.transform. This one change eliminates browser freeze in virtually every old fork.

Hidden Features and Undocumented Behaviors

Search still works when physically buried. Even when the search bar is rotated 180 degrees, buried under the Google logo, or pushed to a corner by collisions, it remains a fully functional HTML <input>. The clickable area follows the physics transform exactly. The physics layer is purely visual — the DOM structure and all event handlers remain intact underneath.

DeviceMotion tilt gravity on MacBook and iOS. Documented in a 2013 Hacker News thread: MacBook Pros with Sudden Motion Sensors expose physical tilt data via the DeviceMotionEvent API. Certain forks respond to literally tilting the hardware — the laptop becomes a physics controller. Requires HTTPS and an explicit user-gesture permission request in modern Chrome and Safari.

Search results physically fall (mirror-specific). On mirror sites with restored search functionality, submitting a query causes result cards to materialize at the top of the viewport and fall into the existing pile, colliding with the floating logo and buttons. This feature was added by mirror developers — it does not exist in the original mrdoob.com version.

Mouse repulsion field (Space variant). In the Zero-Gravity variant, the cursor emits a continuous repulsion force. Moving near elements pushes them away without clicking — implemented by applying outward forces each frame proportional to the inverse square of the cursor-to-body distance.

Body sleeping and wake cascades. Both Box2D and Matter.js implement body sleeping — resting bodies are excluded from all simulation calculations, velocity set to zero, and skipped during the physics step. The cascade is visible: throw one element into a settled pile and watch the collision wake dormant bodies in a chain reaction.

Advanced Extensions

Explosion / Radial Impulse on Double-Click

function explode(originX, originY, radius = 300, strength = 0.05) {
    Composite.allBodies(engine.world).forEach(body => {
        if (body.isStatic) return;
        const dx   = body.position.x - originX;
        const dy   = body.position.y - originY;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist > radius || dist === 0) return;
        const falloff = (1 - dist / radius) * strength;
        Body.applyForce(body, body.position, {
            x: (dx / dist) * falloff,
            y: (dy / dist) * falloff
        });
    });
}

document.addEventListener('dblclick', e => explode(e.clientX, e.clientY));

Gravity Well / Black Hole Attractor

const attractorPos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
const G = 0.00008; // gravitational constant — tune to taste

Events.on(engine, 'beforeUpdate', () => {
    Composite.allBodies(engine.world).forEach(body => {
        if (body.isStatic) return;
        const dx    = attractorPos.x - body.position.x;
        const dy    = attractorPos.y - body.position.y;
        const dist  = Math.max(Math.sqrt(dx * dx + dy * dy), 30); // 30px minimum
        const force = G * body.mass / (dist * dist);
        Body.applyForce(body, body.position, {
            x: (dx / dist) * force,
            y: (dy / dist) * force
        });
    });
});

Mouse Repulsion Field (Space Variant Recreation)

let mouseX = 0, mouseY = 0;
document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });

Events.on(engine, 'beforeUpdate', () => {
    Composite.allBodies(engine.world).forEach(body => {
        if (body.isStatic) return;
        const dx   = body.position.x - mouseX;
        const dy   = body.position.y - mouseY;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist > 150 || dist < 1) return; // active within 150px radius only
        const strength = 0.0003 * body.mass / (dist * dist);
        Body.applyForce(body, body.position, {
            x: (dx / dist) * strength,
            y: (dy / dist) * strength
        });
    });
});

Browser Compatibility Reference (March 2026)

🌐 Compatibility

Browser Compatibility Reference

Physics full = Matter.js runs at 60fps with hardware acceleration. Mirror = use elgoog.im for best experience.

BrowserPhysicsTouchNotes
🟢Chrome 90+
✓ Full◑ LimitedBest overall — hardware acceleration, best rAF scheduling
🦊Firefox 88+
✓ Full◑ LimitedSpiderMonkey slightly slower on intensive physics loops
🧭Safari 14+
✓ Full✓ FullITP may block CDN-hosted physics libraries — self-host to avoid
🔵Edge (Chromium)
✓ Full◑ LimitedSame V8 engine as Chrome — identical performance
💀IE 11
✗ None✗ NoneNo ES6, no CSS transforms API, no Pointer Events — completely broken
📱Mobile Chrome (Android)
◑ Limited⇢ MirrorPerformance limited on mid-range devices — use elgoog.im mirror only
🍎Mobile Safari (iOS)
◑ Limited⇢ MirrorDeviceMotion requires explicit permission via user gesture on iOS 13+
🖼️iframe embed
◑ Limited✗ NoneMouse coordinates require postMessage coordinate remapping — see Hack 9
✓ Full◑ Limited⇢ Mirror✗ NoneMirror = use elgoog.im for that environment

Mistakes Most Developers Make in 2026

Running physics scripts against live google.com. Content Security Policy on google.com will block every injected script without exception. This is not a bug, a browser setting, or an extension conflict. It is an architectural security policy. No debugging, no tool, and no workaround will change this. Use a mirror site.

Initializing on DOMContentLoaded instead of window.load. DOMContentLoaded fires before web fonts and images have finished loading. getBoundingClientRect() captures positions before the final render is complete. Every element appears offset by the height of content that loads after initialization. Switch to window.addEventListener('load', ...) — always.

Using top and left instead of transform for position updates. This is the single most common cause of browser freeze in older forks and custom implementations in 2026. Writing style.top and style.left at 60fps triggers full layout recalculation per frame. Using transform runs on the GPU compositor with zero layout cost. The performance difference is 5–10× and is immediately measurable via Chrome DevTools Performance tab.

Not removing old boundary walls on resize. Adding new walls without removing old ones creates invisible duplicate collision surfaces inside the viewport. Elements begin bouncing off nothing, passing through visible walls, and behaving erratically. Always call World.remove on old walls before World.add with new ones.

Missing the center-to-corner physics offset. Physics engines track body position by center point. CSS positions from the top-left corner. Without subtracting body._halfW and body._halfH from the transform coordinates, every element appears shifted diagonally. The error is invisible in simple test pages and catastrophic in production.

Not null-checking before body creation. Ad blockers remove DOM elements before physics initialization. Attempting to create physics bodies for removed elements causes null reference errors that abort the entire initialization sequence. Always filter by el.offsetParent !== null before processing any element.

Trusting any Google Gravity mirror site beyond known safe domains. Fake mirror sites bundling credential-stealing scripts or cryptocurrency miners with the physics effect are an active threat in 2026. Safe domains: elgoog.im/gravity/ and mrdoob.com. Red flags for malicious mirrors: any login prompt, camera or microphone permission requests, service worker installation, or unusually slow load times.

Skipping prefers-reduced-motion in production. This is an accessibility requirement, not a suggestion. Users with vestibular disorders experience genuine physical discomfort from physics animations. Check window.matchMedia('(prefers-reduced-motion: reduce)') before any initialization. Skip the physics entirely if this media query returns true.


Strategic Conclusion

Google Antigravity — the 2009 browser physics experiment — has outlasted the infrastructure it was built on, the browser paradigms it demonstrated, and the original hosting that ran it. It continues to run in March 2026 because the underlying six-layer architecture is sound: position capture, physics world, DOM detachment, GPU-composited render loop, input handling. Those principles have not aged.

The failure modes documented in this article are not new bugs. They are the same architectural mismatches that have caused the same failures across every fork for seventeen years — layout thrashing from top/left writes, position race conditions from DOMContentLoaded timing, element escape from static boundary walls on resize, and coordinate offset from the center-to-corner physics mismatch. Each is fixable with exactly one code change.

Three new complications define the landscape in March 2026. The naming collision with Google's AI IDE creates search confusion that must be resolved with disambiguation before anything else. The fake mirror site security threat is active and documented. And the experiment's performance patterns — batched reads, GPU-composited transforms, body sleeping — are now directly applicable to production animation, interactive landing pages, and game-like web experiences that extend far beyond the original Google homepage context.

The PHYSICS Method framework in this article represents the current best practice for DOM physics in 2026. The performance optimizations are ranked by verified impact, not convention. The security checklist is non-negotiable for production use.

For casual users: elgoog.im. Nowhere else.

For developers: transform, not top/left. window.load, not DOMContentLoaded. Center minus half-dimension, not center alone. These three corrections fix the majority of all DOM physics implementations that have ever shipped broken.

Share

Frequently Asked Questions

Common questions about this topic

Google Antigravity — the browser physics experiment — is a client-side JavaScript experiment created by Ricardo Cabello (Mr. Doob) in 2009 for Google's Chrome Experiments showcase. It is not an official Google product. It runs exclusively on mirror sites because live Google.com's Content Security Policy blocks all third-party scripts. Note: as of November 2025, 'Google Antigravity' also refers to a completely separate official Google AI IDE product — that product is unrelated to this experiment.
The four most common causes in order: (1) a script-blocking extension is silently preventing the physics library from loading — test in incognito mode first; (2) you are attempting to run it against live google.com, which CSP always blocks — use elgoog.im instead; (3) JavaScript is disabled in browser settings; (4) an old fork is loading the physics library over http://, which modern browsers block as mixed content on HTTPS pages.
Both variants use identical six-layer architecture. The single difference is a gravity vector in Layer 3. Google Gravity sets y: +1 — elements collapse downward, pile on the floor, and can be thrown. Google Anti-Gravity sets y: -1 — elements float upward toward the top of the screen. Google Space sets y: 0 — elements drift weightlessly with cursor repulsion.
The original mrdoob.com version was built exclusively for desktop mouse input. It uses mousedown, mousemove, and mouseup events with no touch fallbacks. Mobile viewports also break the initial body placement coordinate math. The elgoog.im mirror has been rebuilt with the Pointer Events API, which handles mouse, touch, and stylus input uniformly. Always use elgoog.im on mobile.
The 2009 original uses Box2DJS — a JavaScript port of Erin Catto's Box2D C++ engine. Modern mirror sites and custom recreations in 2026 use Matter.js, which works natively in pixel coordinates, has a cleaner API, and performs better at high body counts. For any new DOM physics implementation today, Matter.js is the correct choice.
Using elgoog.im and the archived mrdoob.com is safe. However, as of March 2026, malicious actors actively create fake Google Gravity mirror sites bundling credential-stealing scripts, cryptocurrency miners, and drive-by malware with the legitimate physics effect. Red flags: any site requesting a Google account login, asking for camera or microphone access, prompting a browser extension installation, or taking unusually long to load. Use only elgoog.im/gravity/ or mrdoob.com.
Yes — the physics experiment is not Google-specific. It uses standard CSS selectors and generic DOM APIs that work on any HTML page. The PHYSICS Method framework provides a complete drop-in Matter.js implementation compatible with any webpage. To exclude specific elements, add the data-no-physics attribute to their container.
Almost always caused by position updates writing to style.left and style.top instead of style.transform. The first approach triggers full layout recalculation at 60fps — catastrophically expensive. The second approach runs on the GPU compositor thread with zero layout cost. The 5–10× performance difference is measurable and immediate. Use elgoog.im, which uses the correct transform approach.
Yes. The search bar remains a fully functional HTML input regardless of visual position. Its clickable area follows the physics transform. However, live search results do not work — Google deprecated its Web Search API in 2014, and that pipeline has been broken in all versions since. On well-maintained mirrors like elgoog.im, the search bar still redirects to google.com/search on submission.
They share a name and nothing else. The browser physics experiment was created independently by Ricardo Cabello in 2009 — it has no connection to Google's engineering teams. Google's Antigravity AI IDE is an official product launched November 2025, built on VS Code with Gemini 3, designed for AI-assisted software development. If you are debugging the AI IDE, visit antigravity.google.
🎁

Claim Your Free 2026 AI Starter Kit

Get our definitive guide to the essential AI tools, top prompts, and career templates. Plus, join 10,000+ professionals getting our weekly AI insights.

No spam. Unsubscribe anytime. Powered by Beehiiv.

Explore Related Sections: