What's Happening
Your React app boots up, then immediately dies with this in the console:
Error: Target container is not a DOM element.
React couldn't find its mount point. Either the <div id="root"> never existed, got deleted while you were editing, or โ sneakier โ your script ran before the browser finished parsing the element. Switching from Create React App to Vite is a common trigger, since the two bundlers expect index.html in different places.
Debug Process
Step 1: Check what ReactDOM actually receives
Before changing anything, log the container in your entry file:
// React 18
const container = document.getElementById('root');
console.log(container); // null = the element doesn't exist
ReactDOM.createRoot(container).render(<App />);
// React 16/17
console.log(document.getElementById('root'));
ReactDOM.render(<App />, document.getElementById('root'));
See null in the console? That's your answer. The element isn't there at mount time โ now you just need to find out why.
Step 2: Inspect your index.html
Open public/index.html (CRA) or index.html at the project root (Vite). You're looking for this line:
<div id="root"></div>
Three things that trip people up here:
- The div got deleted during editing โ check for it first
- The
idhas a typo or wrong casing (id="Root"vsid="root") - Your HTML uses
id="app"but your JS callsgetElementById('root')โ or the reverse
Step 3: Check script tag placement in hand-crafted HTML
Not relying on a bundler to inject scripts? The script might fire before the browser finishes parsing the page โ making getElementById return null even when the div is there.
Solutions
Fix 1: Add the missing root div to index.html
Nine times out of ten, this is it. The div vanished during editing. Make sure your HTML has the container with the right ID:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My React App</title>
</head>
<body>
<div id="root"></div>
<!-- Bundler injects the script bundle here -->
</body>
</html>
Then confirm the ID matches exactly in your entry file:
// src/main.jsx (Vite) or src/index.js (CRA)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const container = document.getElementById('root'); // Must match the HTML id
ReactDOM.createRoot(container).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Fix 2: Move the script tag after the root div
Scripts in <head> run before the body is parsed. When loading scripts manually, your JS fires before the browser even reads the <div id="root"> line.
Wrong:
<head>
<script src="bundle.js"></script> <!-- Runs before #root exists -->
</head>
<body>
<div id="root"></div>
</body>
Correct:
<body>
<div id="root"></div>
<script src="bundle.js"></script> <!-- Runs after #root is in the DOM -->
</body>
Prefer keeping scripts in <head>? Add defer. It tells the browser to download the script in parallel and execute it only after HTML parsing completes:
<head>
<script src="bundle.js" defer></script>
</head>
Fix 3: Add a null guard at mount time
CMS-injected pages, browser extensions, and micro-frontend setups don't always guarantee the element exists. Guard against it explicitly:
const container = document.getElementById('root');
if (!container) {
throw new Error(
'Root element not found. Make sure index.html contains <div id="root"></div>'
);
}
ReactDOM.createRoot(container).render(<App />);
A custom error that names the exact missing element beats React's generic DOM complaint every time.
Fix 4: Check index.html location for your bundler
Vite and CRA disagree on where index.html lives. This catches a lot of people mid-migration:
# Vite project structure
my-vite-app/
โโโ index.html <-- must be here, with <div id="root">
โโโ src/
โ โโโ main.jsx
โโโ vite.config.js
# CRA project structure
my-cra-app/
โโโ public/
โ โโโ index.html <-- must be here
โโโ src/
โ โโโ index.js
โโโ package.json
Put index.html in the wrong folder and your bundler either can't find it, or serves a blank page without the root div. Neither gives you a useful error message.
Fix 5: Programmatically create the mount point
Embedding React into a non-React page โ a widget, a third-party plugin โ means the mount element may not exist when your script runs. Build it yourself instead of assuming:
// Fragile: assumes the element already exists
const existingEl = document.getElementById('my-widget');
ReactDOM.createRoot(existingEl).render(<Widget />); // Fails if el is null
// Reliable: create it on the fly
const mountPoint = document.createElement('div');
mountPoint.id = 'my-widget';
document.body.appendChild(mountPoint);
ReactDOM.createRoot(mountPoint).render(<Widget />);
Verify the Fix
Restart your dev server. A clean console โ no red React errors โ is the first sign you're done. Run this in DevTools to double-check:
// Browser DevTools console:
document.getElementById('root'); // Should return the element, not null
Got the element back? React can mount. Still seeing a blank page after this? You're dealing with a separate JS runtime error โ the mount problem is solved, but something else broke.
A one-liner assertion gives you a faster signal during development:
const container = document.getElementById('root');
console.assert(container !== null, '#root element missing from index.html');
ReactDOM.createRoot(container).render(<App />);
Lessons Learned
- JavaScript string comparison is case-sensitive.
"Root"and"root"are not the same ID โ React silently fails on one while the other works fine. - Bundlers (Vite, CRA, webpack) inject scripts at the bottom of
<body>automatically. Hand-crafted HTML doesn't get that treatment, so script placement is your responsibility. - Every bundler migration is a chance for
index.htmlto land in the wrong folder. The 30 seconds spent verifying its location after a CRA โ Vite move saves an hour of head-scratching. - A null guard with a descriptive message at mount time is a gift to your future self. "Root element not found" is a hundred times easier to act on than "Target container is not a DOM element."

