Fix 'Target Container is Not a DOM Element' Error in React App Setup

beginnerโš›๏ธ React2026-06-27| React 16โ€“18, Create React App, Vite, Node.js 14+, all operating systems

Error Message

Error: Target container is not a DOM element.
#react#dom#setup#index.html

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 id has a typo or wrong casing (id="Root" vs id="root")
  • Your HTML uses id="app" but your JS calls getElementById('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.html to 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."

Related Error Notes