diff --git a/packages/react-error-overlay/middleware.js b/packages/react-dev-utils/errorOverlayMiddleware.js
similarity index 76%
rename from packages/react-error-overlay/middleware.js
rename to packages/react-dev-utils/errorOverlayMiddleware.js
index d4fd0d399f1..b2a857d6e37 100644
--- a/packages/react-error-overlay/middleware.js
+++ b/packages/react-dev-utils/errorOverlayMiddleware.js
@@ -8,12 +8,12 @@
  */
 'use strict';
 
-const launchEditor = require('react-dev-utils/launchEditor');
+const launchEditor = require('./launchEditor');
+const launchEditorEndpoint = require('./launchEditorEndpoint');
 
 module.exports = function createLaunchEditorMiddleware() {
   return function launchEditorMiddleware(req, res, next) {
-    // Keep this in sync with react-error-overlay
-    if (req.url.startsWith('/__open-stack-frame-in-editor')) {
+    if (req.url.startsWith(launchEditorEndpoint)) {
       launchEditor(req.query.fileName, req.query.lineNumber);
       res.end();
     } else {
diff --git a/packages/react-error-overlay/src/utils/dom/consumeEvent.js b/packages/react-dev-utils/launchEditorEndpoint.js
similarity index 65%
rename from packages/react-error-overlay/src/utils/dom/consumeEvent.js
rename to packages/react-dev-utils/launchEditorEndpoint.js
index 90bb9d1e815..e21870be9c4 100644
--- a/packages/react-error-overlay/src/utils/dom/consumeEvent.js
+++ b/packages/react-dev-utils/launchEditorEndpoint.js
@@ -6,13 +6,7 @@
  * LICENSE file in the root directory of this source tree. An additional grant
  * of patent rights can be found in the PATENTS file in the same directory.
  */
+'use strict';
 
-/* @flow */
-function consumeEvent(e: Event) {
-  e.preventDefault();
-  if (typeof e.target.blur === 'function') {
-    e.target.blur();
-  }
-}
-
-export { consumeEvent };
+// TODO: we might want to make this injectable to support DEV-time non-root URLs.
+module.exports = '/__open-stack-frame-in-editor';
diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json
index 8816e4009a6..ccc22bf6332 100644
--- a/packages/react-dev-utils/package.json
+++ b/packages/react-dev-utils/package.json
@@ -11,12 +11,12 @@
     "node": ">=6"
   },
   "files": [
-    "ansiHTML.js",
     "checkRequiredFiles.js",
     "clearConsole.js",
     "crashOverlay.js",
     "crossSpawn.js",
     "eslintFormatter.js",
+    "errorOverlayMiddleware.js",
     "FileSizeReporter.js",
     "printBuildError.js",
     "formatWebpackMessages.js",
@@ -24,6 +24,7 @@
     "inquirer.js",
     "InterpolateHtmlPlugin.js",
     "launchEditor.js",
+    "launchEditorEndpoint.js",
     "ModuleScopePlugin.js",
     "noopServiceWorkerMiddleware.js",
     "openBrowser.js",
@@ -35,7 +36,6 @@
   ],
   "dependencies": {
     "address": "1.0.2",
-    "anser": "1.4.1",
     "babel-code-frame": "6.22.0",
     "chalk": "1.1.3",
     "cross-spawn": "5.1.0",
@@ -44,10 +44,10 @@
     "filesize": "3.5.10",
     "global-modules": "1.0.0",
     "gzip-size": "3.0.0",
-    "html-entities": "1.2.1",
     "inquirer": "3.2.1",
     "is-root": "1.0.0",
     "opn": "5.1.0",
+    "react-error-overlay": "^1.0.9",
     "recursive-readdir": "2.2.1",
     "shell-quote": "1.6.1",
     "sockjs-client": "1.1.4",
diff --git a/packages/react-dev-utils/webpackHotDevClient.js b/packages/react-dev-utils/webpackHotDevClient.js
index f2f206a5cd2..b6effe718e5 100644
--- a/packages/react-dev-utils/webpackHotDevClient.js
+++ b/packages/react-dev-utils/webpackHotDevClient.js
@@ -21,143 +21,27 @@
 var SockJS = require('sockjs-client');
 var stripAnsi = require('strip-ansi');
 var url = require('url');
+var launchEditorEndpoint = require('./launchEditorEndpoint');
 var formatWebpackMessages = require('./formatWebpackMessages');
-var ansiHTML = require('./ansiHTML');
-
-function createOverlayIframe(onIframeLoad) {
-  var iframe = document.createElement('iframe');
-  iframe.id = 'react-dev-utils-webpack-hot-dev-client-overlay';
-  iframe.src = 'about:blank';
-  iframe.style.position = 'fixed';
-  iframe.style.left = 0;
-  iframe.style.top = 0;
-  iframe.style.right = 0;
-  iframe.style.bottom = 0;
-  iframe.style.width = '100vw';
-  iframe.style.height = '100vh';
-  iframe.style.border = 'none';
-  iframe.style.zIndex = 2147483647;
-  iframe.onload = onIframeLoad;
-  return iframe;
-}
-
-function addOverlayDivTo(iframe) {
-  // TODO: unify these styles with react-error-overlay
-  iframe.contentDocument.body.style.margin = 0;
-  iframe.contentDocument.body.style.maxWidth = '100vw';
-
-  var outerDiv = iframe.contentDocument.createElement('div');
-  outerDiv.id = 'react-dev-utils-webpack-hot-dev-client-overlay-div';
-  outerDiv.style.width = '100%';
-  outerDiv.style.height = '100%';
-  outerDiv.style.boxSizing = 'border-box';
-  outerDiv.style.textAlign = 'center';
-  outerDiv.style.backgroundColor = 'rgb(255, 255, 255)';
-
-  var div = iframe.contentDocument.createElement('div');
-  div.style.position = 'relative';
-  div.style.display = 'inline-flex';
-  div.style.flexDirection = 'column';
-  div.style.height = '100%';
-  div.style.width = '1024px';
-  div.style.maxWidth = '100%';
-  div.style.overflowX = 'hidden';
-  div.style.overflowY = 'auto';
-  div.style.padding = '0.5rem';
-  div.style.boxSizing = 'border-box';
-  div.style.textAlign = 'left';
-  div.style.fontFamily = 'Consolas, Menlo, monospace';
-  div.style.fontSize = '11px';
-  div.style.whiteSpace = 'pre-wrap';
-  div.style.wordBreak = 'break-word';
-  div.style.lineHeight = '1.5';
-  div.style.color = 'rgb(41, 50, 56)';
-
-  outerDiv.appendChild(div);
-  iframe.contentDocument.body.appendChild(outerDiv);
-  return div;
-}
-
-function overlayHeaderStyle() {
-  return (
-    'font-size: 2em;' +
-    'font-family: sans-serif;' +
-    'color: rgb(206, 17, 38);' +
-    'white-space: pre-wrap;' +
-    'margin: 0 2rem 0.75rem 0px;' +
-    'flex: 0 0 auto;' +
-    'max-height: 35%;' +
-    'overflow: auto;'
-  );
-}
-
-var overlayIframe = null;
-var overlayDiv = null;
-var lastOnOverlayDivReady = null;
-
-function ensureOverlayDivExists(onOverlayDivReady) {
-  if (overlayDiv) {
-    // Everything is ready, call the callback right away.
-    onOverlayDivReady(overlayDiv);
-    return;
-  }
-
-  // Creating an iframe may be asynchronous so we'll schedule the callback.
-  // In case of multiple calls, last callback wins.
-  lastOnOverlayDivReady = onOverlayDivReady;
-
-  if (overlayIframe) {
-    // We're already creating it.
-    return;
-  }
-
-  // Create iframe and, when it is ready, a div inside it.
-  overlayIframe = createOverlayIframe(function onIframeLoad() {
-    overlayDiv = addOverlayDivTo(overlayIframe);
-    // Now we can talk!
-    lastOnOverlayDivReady(overlayDiv);
-  });
-
-  // Zalgo alert: onIframeLoad() will be called either synchronously
-  // or asynchronously depending on the browser.
-  // We delay adding it so `overlayIframe` is set when `onIframeLoad` fires.
-  document.body.appendChild(overlayIframe);
-}
+var ErrorOverlay = require('react-error-overlay');
+
+ErrorOverlay.startReportingRuntimeErrors({
+  launchEditorEndpoint: launchEditorEndpoint,
+  onError: function() {
+    // TODO: why do we need this?
+    if (module.hot && typeof module.hot.decline === 'function') {
+      module.hot.decline();
+    }
+  },
+});
 
-function showErrorOverlay(message) {
-  ensureOverlayDivExists(function onOverlayDivReady(overlayDiv) {
-    // TODO: unify this with our runtime overlay
-    overlayDiv.innerHTML =
-      '<div style="' +
-      overlayHeaderStyle() +
-      '">Failed to compile</div>' +
-      '<pre style="' +
-      'display: block; padding: 0.5em; margin-top: 0; ' +
-      'margin-bottom: 0.5em; overflow-x: auto; white-space: pre-wrap; ' +
-      'border-radius: 0.25rem; background-color: rgba(206, 17, 38, 0.05)">' +
-      '<code style="font-family: Consolas, Menlo, monospace;">' +
-      ansiHTML(message) +
-      '</code></pre>' +
-      '<div style="' +
-      'font-family: sans-serif; color: rgb(135, 142, 145); margin-top: 0.5rem; ' +
-      'flex: 0 0 auto">' +
-      'This error occurred during the build time and cannot be dismissed.</div>';
+if (module.hot && typeof module.hot.dispose === 'function') {
+  module.hot.dispose(function() {
+    // TODO: why do we need this?
+    ErrorOverlay.stopReportingRuntimeErrors();
   });
 }
 
-function destroyErrorOverlay() {
-  if (!overlayDiv) {
-    // It is not there in the first place.
-    return;
-  }
-
-  // Clean up and reset internal state.
-  document.body.removeChild(overlayIframe);
-  overlayDiv = null;
-  overlayIframe = null;
-  lastOnOverlayDivReady = null;
-}
-
 // Connect to WebpackDevServer via a socket.
 var connection = new SockJS(
   url.format({
@@ -205,9 +89,9 @@ function handleSuccess() {
   // Attempt to apply hot updates or reload.
   if (isHotUpdate) {
     tryApplyUpdates(function onHotUpdateSuccess() {
-      // Only destroy it when we're sure it's a hot update.
+      // Only dismiss it when we're sure it's a hot update.
       // Otherwise it would flicker right before the reload.
-      destroyErrorOverlay();
+      ErrorOverlay.dismissBuildError();
     });
   }
 }
@@ -247,9 +131,9 @@ function handleWarnings(warnings) {
       // Only print warnings if we aren't refreshing the page.
       // Otherwise they'll disappear right away anyway.
       printWarnings();
-      // Only destroy it when we're sure it's a hot update.
+      // Only dismiss it when we're sure it's a hot update.
       // Otherwise it would flicker right before the reload.
-      destroyErrorOverlay();
+      ErrorOverlay.dismissBuildError();
     });
   } else {
     // Print initial warnings immediately.
@@ -271,7 +155,7 @@ function handleErrors(errors) {
   });
 
   // Only show the first error.
-  showErrorOverlay(formatted.errors[0]);
+  ErrorOverlay.reportBuildError(formatted.errors[0]);
 
   // Also log them to the console.
   if (typeof console !== 'undefined' && typeof console.error === 'function') {
diff --git a/packages/react-error-overlay/.flowconfig b/packages/react-error-overlay/.flowconfig
index 261b8646fc3..8d7de784e29 100644
--- a/packages/react-error-overlay/.flowconfig
+++ b/packages/react-error-overlay/.flowconfig
@@ -1,4 +1,5 @@
 [ignore]
+.*/node_modules/eslint-plugin-jsx-a11y/.*
 
 [include]
 src/**/*.js
diff --git a/packages/react-error-overlay/fixtures/bundle.json b/packages/react-error-overlay/fixtures/bundle.json
index 7dfd31f5863..16670f6231f 100644
--- a/packages/react-error-overlay/fixtures/bundle.json
+++ b/packages/react-error-overlay/fixtures/bundle.json
@@ -240,11 +240,11 @@
     ]
   },
   {
-    "functionName": "Object.batchedUpdates",
+    "functionName": "batchedUpdates",
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 33900,
     "columnNumber": 26,
-    "_originalFunctionName": "Object.batchedUpdates",
+    "_originalFunctionName": "batchedUpdates",
     "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactDefaultBatchingStrategy.js",
     "_originalLineNumber": 62,
     "_originalColumnNumber": 0,
@@ -264,11 +264,11 @@
     ]
   },
   {
-    "functionName": "Object.batchedUpdates",
+    "functionName": "batchedUpdates",
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 2181,
     "columnNumber": 27,
-    "_originalFunctionName": "Object.batchedUpdates",
+    "_originalFunctionName": "batchedUpdates",
     "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactUpdates.js",
     "_originalLineNumber": 97,
     "_originalColumnNumber": 0,
@@ -288,11 +288,11 @@
     ]
   },
   {
-    "functionName": "Object._renderNewRootComponent",
+    "functionName": "_renderNewRootComponent",
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 14398,
     "columnNumber": 18,
-    "_originalFunctionName": "Object._renderNewRootComponent",
+    "_originalFunctionName": "_renderNewRootComponent",
     "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactMount.js",
     "_originalLineNumber": 320,
     "_originalColumnNumber": 0,
@@ -312,11 +312,11 @@
     ]
   },
   {
-    "functionName": "Object._renderSubtreeIntoContainer",
+    "functionName": "_renderSubtreeIntoContainer",
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 14479,
     "columnNumber": 32,
-    "_originalFunctionName": "Object._renderSubtreeIntoContainer",
+    "_originalFunctionName": "_renderSubtreeIntoContainer",
     "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactMount.js",
     "_originalLineNumber": 401,
     "_originalColumnNumber": 0,
@@ -336,11 +336,11 @@
     ]
   },
   {
-    "functionName": "Object.render",
+    "functionName": "render",
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 14500,
     "columnNumber": 23,
-    "_originalFunctionName": "Object.render",
+    "_originalFunctionName": "render",
     "_originalFileName": "webpack:///packages/react-scripts/~/react-dom/lib/ReactMount.js",
     "_originalLineNumber": 422,
     "_originalColumnNumber": 0,
@@ -360,11 +360,11 @@
     ]
   },
   {
-    "functionName": "Object.friendlySyntaxErrorLabel",
+    "functionName": null,
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 17287,
     "columnNumber": 20,
-    "_originalFunctionName": "Object.friendlySyntaxErrorLabel",
+    "_originalFunctionName": null,
     "_originalFileName": "webpack:///packages/react-scripts/template/src/index.js",
     "_originalLineNumber": 6,
     "_originalColumnNumber": 0,
@@ -432,11 +432,11 @@
     ]
   },
   {
-    "functionName": "Object.<anonymous>",
+    "functionName": null,
     "fileName": "http://localhost:3000/static/js/bundle.js",
     "lineNumber": 41219,
     "columnNumber": 18,
-    "_originalFunctionName": "Object.<anonymous>",
+    "_originalFunctionName": null,
     "_originalFileName": null,
     "_originalLineNumber": null,
     "_originalColumnNumber": null,
diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json
index 5a9e864c50f..65c9b5efa0a 100644
--- a/packages/react-error-overlay/package.json
+++ b/packages/react-error-overlay/package.json
@@ -34,7 +34,9 @@
     "anser": "1.4.1",
     "babel-code-frame": "6.22.0",
     "babel-runtime": "6.23.0",
-    "react-dev-utils": "^3.1.0",
+    "html-entities": "1.2.1",
+    "react": "^15.5.4",
+    "react-dom": "^15.5.4",
     "settle-promise": "1.0.0",
     "source-map": "0.5.6"
   },
diff --git a/packages/react-error-overlay/src/__tests__/stack-frame.js b/packages/react-error-overlay/src/__tests__/stack-frame.js
index dc6a01b4a06..5a015260ab2 100644
--- a/packages/react-error-overlay/src/__tests__/stack-frame.js
+++ b/packages/react-error-overlay/src/__tests__/stack-frame.js
@@ -13,9 +13,9 @@ test('proper empty shape', () => {
   const empty = new StackFrame();
   expect(empty).toMatchSnapshot();
 
-  expect(empty.getFunctionName()).toBe(null);
+  expect(empty.getFunctionName()).toBe('(anonymous function)');
   expect(empty.getSource()).toBe('');
-  expect(empty.toString()).toBe('');
+  expect(empty.toString()).toBe('(anonymous function)');
 });
 
 test('proper full shape', () => {
diff --git a/packages/react-error-overlay/src/components/CloseButton.js b/packages/react-error-overlay/src/components/CloseButton.js
new file mode 100644
index 00000000000..503b1198c3f
--- /dev/null
+++ b/packages/react-error-overlay/src/components/CloseButton.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React from 'react';
+import { black } from '../styles';
+
+const closeButtonStyle = {
+  color: black,
+  lineHeight: '1rem',
+  fontSize: '1.5rem',
+  padding: '1rem',
+  cursor: 'pointer',
+  position: 'absolute',
+  right: 0,
+  top: 0,
+};
+
+type CloseCallback = () => void;
+function CloseButton({ close }: {| close: CloseCallback |}) {
+  return (
+    <span
+      title="Click or press Escape to dismiss."
+      onClick={close}
+      style={closeButtonStyle}
+    >
+      ×
+    </span>
+  );
+}
+
+export default CloseButton;
diff --git a/packages/react-error-overlay/src/components/CodeBlock.js b/packages/react-error-overlay/src/components/CodeBlock.js
new file mode 100644
index 00000000000..478f0111b9b
--- /dev/null
+++ b/packages/react-error-overlay/src/components/CodeBlock.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React from 'react';
+import { redTransparent, yellowTransparent } from '../styles';
+
+const _preStyle = {
+  display: 'block',
+  padding: '0.5em',
+  marginTop: '0.5em',
+  marginBottom: '0.5em',
+  overflowX: 'auto',
+  whiteSpace: 'pre-wrap',
+  borderRadius: '0.25rem',
+};
+
+const primaryPreStyle = {
+  ..._preStyle,
+  backgroundColor: redTransparent,
+};
+
+const secondaryPreStyle = {
+  ..._preStyle,
+  backgroundColor: yellowTransparent,
+};
+
+const codeStyle = {
+  fontFamily: 'Consolas, Menlo, monospace',
+};
+
+type CodeBlockPropsType = {|
+  main: boolean,
+  codeHTML: string,
+|};
+
+function CodeBlock(props: CodeBlockPropsType) {
+  const preStyle = props.main ? primaryPreStyle : secondaryPreStyle;
+  const codeBlock = { __html: props.codeHTML };
+
+  return (
+    <pre style={preStyle}>
+      <code style={codeStyle} dangerouslySetInnerHTML={codeBlock} />
+    </pre>
+  );
+}
+
+export default CodeBlock;
diff --git a/packages/react-error-overlay/src/components/Collapsible.js b/packages/react-error-overlay/src/components/Collapsible.js
new file mode 100644
index 00000000000..92f1de4295c
--- /dev/null
+++ b/packages/react-error-overlay/src/components/Collapsible.js
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React, { Component } from 'react';
+import { black } from '../styles';
+
+const _collapsibleStyle = {
+  color: black,
+  cursor: 'pointer',
+  border: 'none',
+  display: 'block',
+  width: '100%',
+  textAlign: 'left',
+  background: '#fff',
+  fontFamily: 'Consolas, Menlo, monospace',
+  fontSize: '1em',
+  padding: '0px',
+  lineHeight: '1.5',
+};
+
+const collapsibleCollapsedStyle = {
+  ..._collapsibleStyle,
+  marginBottom: '1.5em',
+};
+
+const collapsibleExpandedStyle = {
+  ..._collapsibleStyle,
+  marginBottom: '0.6em',
+};
+
+class Collapsible extends Component {
+  state = {
+    collapsed: true,
+  };
+
+  toggleCollaped = () => {
+    this.setState(state => ({
+      collapsed: !state.collapsed,
+    }));
+  };
+
+  render() {
+    const count = this.props.children.length;
+    const collapsed = this.state.collapsed;
+    return (
+      <div>
+        <button
+          onClick={this.toggleCollaped}
+          style={
+            collapsed ? collapsibleCollapsedStyle : collapsibleExpandedStyle
+          }
+        >
+          {(collapsed ? '▶' : '▼') +
+            ` ${count} stack frames were ` +
+            (collapsed ? 'collapsed.' : 'expanded.')}
+        </button>
+        <div style={{ display: collapsed ? 'none' : 'block' }}>
+          {this.props.children}
+          <button
+            onClick={this.toggleCollaped}
+            style={collapsibleExpandedStyle}
+          >
+            {`▲ ${count} stack frames were expanded.`}
+          </button>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Collapsible;
diff --git a/packages/react-error-overlay/src/components/Footer.js b/packages/react-error-overlay/src/components/Footer.js
new file mode 100644
index 00000000000..68eb8465674
--- /dev/null
+++ b/packages/react-error-overlay/src/components/Footer.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React from 'react';
+import { darkGray } from '../styles';
+
+const footerStyle = {
+  fontFamily: 'sans-serif',
+  color: darkGray,
+  marginTop: '0.5rem',
+  flex: '0 0 auto',
+};
+
+type FooterPropsType = {|
+  line1: string,
+  line2?: string,
+|};
+
+function Footer(props: FooterPropsType) {
+  return (
+    <div style={footerStyle}>
+      {props.line1}
+      <br />
+      {props.line2}
+    </div>
+  );
+}
+
+export default Footer;
diff --git a/packages/react-error-overlay/src/components/Header.js b/packages/react-error-overlay/src/components/Header.js
new file mode 100644
index 00000000000..a2f40973d00
--- /dev/null
+++ b/packages/react-error-overlay/src/components/Header.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React from 'react';
+import { red } from '../styles';
+
+const headerStyle = {
+  fontSize: '2em',
+  fontFamily: 'sans-serif',
+  color: red,
+  whiteSpace: 'pre-wrap',
+  // Top bottom margin spaces header
+  // Right margin revents overlap with close button
+  margin: '0 2rem 0.75rem 0',
+  flex: '0 0 auto',
+  maxHeight: '50%',
+  overflow: 'auto',
+};
+
+type HeaderPropType = {|
+  headerText: string,
+|};
+
+function Header(props: HeaderPropType) {
+  return (
+    <div style={headerStyle}>
+      {props.headerText}
+    </div>
+  );
+}
+
+export default Header;
diff --git a/packages/react-error-overlay/src/components/NavigationBar.js b/packages/react-error-overlay/src/components/NavigationBar.js
new file mode 100644
index 00000000000..4eba743cef7
--- /dev/null
+++ b/packages/react-error-overlay/src/components/NavigationBar.js
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React from 'react';
+import { red, redTransparent } from '../styles';
+
+const navigationBarStyle = {
+  marginBottom: '0.5rem',
+};
+
+const buttonContainerStyle = {
+  marginRight: '1em',
+};
+
+const _navButtonStyle = {
+  backgroundColor: redTransparent,
+  color: red,
+  border: 'none',
+  borderRadius: '4px',
+  padding: '3px 6px',
+  cursor: 'pointer',
+};
+
+const leftButtonStyle = {
+  ..._navButtonStyle,
+  borderTopRightRadius: '0px',
+  borderBottomRightRadius: '0px',
+  marginRight: '1px',
+};
+
+const rightButtonStyle = {
+  ..._navButtonStyle,
+  borderTopLeftRadius: '0px',
+  borderBottomLeftRadius: '0px',
+};
+
+type Callback = () => void;
+
+type NavigationBarPropsType = {|
+  currentError: number,
+  totalErrors: number,
+  previous: Callback,
+  next: Callback,
+|};
+
+function NavigationBar(props: NavigationBarPropsType) {
+  const { currentError, totalErrors, previous, next } = props;
+  return (
+    <div style={navigationBarStyle}>
+      <span style={buttonContainerStyle}>
+        <button onClick={previous} style={leftButtonStyle}>
+          ←
+        </button>
+        <button onClick={next} style={rightButtonStyle}>
+          →
+        </button>
+      </span>
+      {`${currentError} of ${totalErrors} errors on the page`}
+    </div>
+  );
+}
+
+export default NavigationBar;
diff --git a/packages/react-error-overlay/src/components/Overlay.js b/packages/react-error-overlay/src/components/Overlay.js
new file mode 100644
index 00000000000..4fe530b6fee
--- /dev/null
+++ b/packages/react-error-overlay/src/components/Overlay.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React, { Component } from 'react';
+import { black } from '../styles';
+
+const overlayStyle = {
+  position: 'relative',
+  display: 'inline-flex',
+  flexDirection: 'column',
+  height: '100%',
+  width: '1024px',
+  maxWidth: '100%',
+  overflowX: 'hidden',
+  overflowY: 'auto',
+  padding: '0.5rem',
+  boxSizing: 'border-box',
+  textAlign: 'left',
+  fontFamily: 'Consolas, Menlo, monospace',
+  fontSize: '11px',
+  whiteSpace: 'pre-wrap',
+  wordBreak: 'break-word',
+  lineHeight: 1.5,
+  color: black,
+};
+
+class Overlay extends Component {
+  iframeWindow: window = null;
+
+  getIframeWindow = (element: HTMLDivElement) => {
+    if (element) {
+      const document = element.ownerDocument;
+      this.iframeWindow = document.defaultView;
+    }
+  };
+
+  onKeyDown = (e: KeyboardEvent) => {
+    const { shortcutHandler } = this.props;
+    if (shortcutHandler) {
+      shortcutHandler(e.key);
+    }
+  };
+
+  componentDidMount() {
+    window.addEventListener('keydown', this.onKeyDown);
+    if (this.iframeWindow) {
+      this.iframeWindow.addEventListener('keydown', this.onKeyDown);
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('keydown', this.onKeyDown);
+    if (this.iframeWindow) {
+      this.iframeWindow.removeEventListener('keydown', this.onKeyDown);
+    }
+  }
+
+  render() {
+    return (
+      <div style={overlayStyle} ref={this.getIframeWindow}>
+        {this.props.children}
+      </div>
+    );
+  }
+}
+
+export default Overlay;
diff --git a/packages/react-error-overlay/src/components/additional.js b/packages/react-error-overlay/src/components/additional.js
deleted file mode 100644
index b573c740634..00000000000
--- a/packages/react-error-overlay/src/components/additional.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import { applyStyles } from '../utils/dom/css';
-import {
-  additionalChildStyle,
-  groupStyle,
-  groupElemLeft,
-  groupElemRight,
-} from '../styles';
-import { consumeEvent } from '../utils/dom/consumeEvent';
-import { enableTabClick } from '../utils/dom/enableTabClick';
-
-type SwitchCallback = (offset: number) => void;
-function updateAdditional(
-  document: Document,
-  additionalReference: HTMLDivElement,
-  currentError: number,
-  totalErrors: number,
-  switchCallback: SwitchCallback
-) {
-  if (additionalReference.lastChild) {
-    additionalReference.removeChild(additionalReference.lastChild);
-  }
-
-  if (totalErrors <= 1) {
-    return;
-  }
-
-  const div = document.createElement('div');
-  applyStyles(div, additionalChildStyle);
-
-  const group = document.createElement('span');
-  applyStyles(group, groupStyle);
-
-  const left = document.createElement('button');
-  applyStyles(left, groupElemLeft);
-  left.addEventListener('click', function(e: MouseEvent) {
-    consumeEvent(e);
-    switchCallback(-1);
-  });
-  left.appendChild(document.createTextNode('←'));
-  enableTabClick(left);
-
-  const right = document.createElement('button');
-  applyStyles(right, groupElemRight);
-  right.addEventListener('click', function(e: MouseEvent) {
-    consumeEvent(e);
-    switchCallback(1);
-  });
-  right.appendChild(document.createTextNode('→'));
-  enableTabClick(right);
-
-  group.appendChild(left);
-  group.appendChild(right);
-  div.appendChild(group);
-
-  const text = `${currentError} of ${totalErrors} errors on the page`;
-  div.appendChild(document.createTextNode(text));
-
-  additionalReference.appendChild(div);
-}
-
-export type { SwitchCallback };
-export { updateAdditional };
diff --git a/packages/react-error-overlay/src/components/close.js b/packages/react-error-overlay/src/components/close.js
deleted file mode 100644
index 2ced8d0ce92..00000000000
--- a/packages/react-error-overlay/src/components/close.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import { applyStyles } from '../utils/dom/css';
-import { hintsStyle, hintStyle, closeButtonStyle } from '../styles';
-
-function createHint(document: Document, hint: string, title: string) {
-  const span = document.createElement('span');
-  span.appendChild(document.createTextNode(hint));
-  span.setAttribute('title', title);
-  applyStyles(span, hintStyle);
-  return span;
-}
-
-type CloseCallback = () => void;
-function createClose(document: Document, callback: CloseCallback) {
-  const hints = document.createElement('div');
-  applyStyles(hints, hintsStyle);
-
-  const close = createHint(document, '×', 'Click or press Escape to dismiss.');
-  close.addEventListener('click', () => callback());
-  applyStyles(close, closeButtonStyle);
-  hints.appendChild(close);
-  return hints;
-}
-
-export type { CloseCallback };
-export { createClose };
diff --git a/packages/react-error-overlay/src/components/footer.js b/packages/react-error-overlay/src/components/footer.js
deleted file mode 100644
index 4586a04ff2b..00000000000
--- a/packages/react-error-overlay/src/components/footer.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import { applyStyles } from '../utils/dom/css';
-import { footerStyle } from '../styles';
-
-function createFooter(document: Document) {
-  const div = document.createElement('div');
-  applyStyles(div, footerStyle);
-  div.appendChild(
-    document.createTextNode(
-      'This screen is visible only in development. It will not appear if the app crashes in production.'
-    )
-  );
-  div.appendChild(document.createElement('br'));
-  div.appendChild(
-    document.createTextNode(
-      'Open your browser’s developer console to further inspect this error.'
-    )
-  );
-  return div;
-}
-
-export { createFooter };
diff --git a/packages/react-error-overlay/src/components/frame.js b/packages/react-error-overlay/src/components/frame.js
deleted file mode 100644
index 4087f4d9c9d..00000000000
--- a/packages/react-error-overlay/src/components/frame.js
+++ /dev/null
@@ -1,355 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import { enableTabClick } from '../utils/dom/enableTabClick';
-import { createCode } from './code';
-import { isInternalFile } from '../utils/isInternalFile';
-import type { StackFrame } from '../utils/stack-frame';
-import type { FrameSetting, OmitsObject } from './frames';
-import { applyStyles } from '../utils/dom/css';
-import {
-  omittedFramesExpandedStyle,
-  omittedFramesCollapsedStyle,
-  functionNameStyle,
-  depStyle,
-  linkStyle,
-  anchorStyle,
-  hiddenStyle,
-} from '../styles';
-
-function getGroupToggle(
-  document: Document,
-  omitsCount: number,
-  omitBundle: number
-) {
-  const omittedFrames = document.createElement('div');
-  enableTabClick(omittedFrames);
-  const text1 = document.createTextNode(
-    '\u25B6 ' + omitsCount + ' stack frames were collapsed.'
-  );
-  omittedFrames.appendChild(text1);
-  omittedFrames.addEventListener('click', function() {
-    const hide = text1.textContent.match(/▲/);
-    const list = document.getElementsByName('bundle-' + omitBundle);
-    for (let index = 0; index < list.length; ++index) {
-      const n = list[index];
-      if (hide) {
-        n.style.display = 'none';
-      } else {
-        n.style.display = '';
-      }
-    }
-    if (hide) {
-      text1.textContent = text1.textContent.replace(/▲/, '▶');
-      text1.textContent = text1.textContent.replace(/expanded/, 'collapsed');
-      applyStyles(omittedFrames, omittedFramesCollapsedStyle);
-    } else {
-      text1.textContent = text1.textContent.replace(/▶/, '▲');
-      text1.textContent = text1.textContent.replace(/collapsed/, 'expanded');
-      applyStyles(omittedFrames, omittedFramesExpandedStyle);
-    }
-  });
-  applyStyles(omittedFrames, omittedFramesCollapsedStyle);
-  return omittedFrames;
-}
-
-function insertBeforeBundle(
-  document: Document,
-  parent: Node,
-  omitsCount: number,
-  omitBundle: number,
-  actionElement
-) {
-  const children = document.getElementsByName('bundle-' + omitBundle);
-  if (children.length < 1) {
-    return;
-  }
-  let first: ?Node = children[0];
-  while (first != null && first.parentNode !== parent) {
-    first = first.parentNode;
-  }
-  const div = document.createElement('div');
-  enableTabClick(div);
-  div.setAttribute('name', 'bundle-' + omitBundle);
-  const text = document.createTextNode(
-    '\u25BC ' + omitsCount + ' stack frames were expanded.'
-  );
-  div.appendChild(text);
-  div.addEventListener('click', function() {
-    return actionElement.click();
-  });
-  applyStyles(div, omittedFramesExpandedStyle);
-  div.style.display = 'none';
-
-  parent.insertBefore(div, first);
-}
-
-function frameDiv(
-  document: Document,
-  functionName,
-  url,
-  internalUrl,
-  onSourceClick: ?Function
-) {
-  const frame = document.createElement('div');
-  const frameFunctionName = document.createElement('div');
-
-  if (functionName && functionName.indexOf('Object.') === 0) {
-    functionName = functionName.slice('Object.'.length);
-  }
-  if (functionName === '<anonymous>') {
-    functionName = null;
-  }
-  const cleanedFunctionName = functionName || '(anonymous function)';
-  const cleanedUrl = url.replace('webpack://', '.');
-
-  if (internalUrl) {
-    applyStyles(
-      frameFunctionName,
-      Object.assign({}, functionNameStyle, depStyle)
-    );
-  } else {
-    applyStyles(frameFunctionName, functionNameStyle);
-  }
-
-  frameFunctionName.appendChild(document.createTextNode(cleanedFunctionName));
-  frame.appendChild(frameFunctionName);
-
-  const frameLink = document.createElement('div');
-  applyStyles(frameLink, linkStyle);
-  const frameAnchor = document.createElement('a');
-  applyStyles(frameAnchor, anchorStyle);
-  frameAnchor.appendChild(document.createTextNode(cleanedUrl));
-  frameLink.appendChild(frameAnchor);
-  frame.appendChild(frameLink);
-
-  if (typeof onSourceClick === 'function') {
-    let handler = onSourceClick;
-    enableTabClick(frameAnchor);
-    frameAnchor.style.cursor = 'pointer';
-    frameAnchor.addEventListener('click', function() {
-      handler();
-    });
-  }
-
-  return frame;
-}
-
-function isBultinErrorName(errorName: ?string) {
-  switch (errorName) {
-    case 'EvalError':
-    case 'InternalError':
-    case 'RangeError':
-    case 'ReferenceError':
-    case 'SyntaxError':
-    case 'TypeError':
-    case 'URIError':
-      return true;
-    default:
-      return false;
-  }
-}
-
-function getPrettyURL(
-  sourceFileName: ?string,
-  sourceLineNumber: ?number,
-  sourceColumnNumber: ?number,
-  fileName: ?string,
-  lineNumber: ?number,
-  columnNumber: ?number,
-  compiled: boolean
-): string {
-  let prettyURL;
-  if (!compiled && sourceFileName && typeof sourceLineNumber === 'number') {
-    // Remove everything up to the first /src/ or /node_modules/
-    const trimMatch = /^[/|\\].*?[/|\\]((src|node_modules)[/|\\].*)/.exec(
-      sourceFileName
-    );
-    if (trimMatch && trimMatch[1]) {
-      prettyURL = trimMatch[1];
-    } else {
-      prettyURL = sourceFileName;
-    }
-    prettyURL += ':' + sourceLineNumber;
-    // Note: we intentionally skip 0's because they're produced by cheap Webpack maps
-    if (sourceColumnNumber) {
-      prettyURL += ':' + sourceColumnNumber;
-    }
-  } else if (fileName && typeof lineNumber === 'number') {
-    prettyURL = fileName + ':' + lineNumber;
-    // Note: we intentionally skip 0's because they're produced by cheap Webpack maps
-    if (columnNumber) {
-      prettyURL += ':' + columnNumber;
-    }
-  } else {
-    prettyURL = 'unknown';
-  }
-  return prettyURL;
-}
-
-function createFrame(
-  document: Document,
-  frameSetting: FrameSetting,
-  frame: StackFrame,
-  contextSize: number,
-  critical: boolean,
-  omits: OmitsObject,
-  omitBundle: number,
-  parentContainer: HTMLDivElement,
-  lastElement: boolean,
-  errorName: ?string
-) {
-  const { compiled } = frameSetting;
-  let { functionName, _originalFileName: sourceFileName } = frame;
-  const {
-    fileName,
-    lineNumber,
-    columnNumber,
-    _scriptCode: scriptLines,
-    _originalLineNumber: sourceLineNumber,
-    _originalColumnNumber: sourceColumnNumber,
-    _originalScriptCode: sourceLines,
-  } = frame;
-
-  // TODO: find a better place for this.
-  // Chrome has a bug with inferring function.name:
-  // https://github.com/facebookincubator/create-react-app/issues/2097
-  // Let's ignore a meaningless name we get for top-level modules.
-  if (
-    functionName === 'Object.friendlySyntaxErrorLabel' ||
-    functionName === 'Object.exports.__esModule'
-  ) {
-    functionName = '(anonymous function)';
-  }
-
-  const prettyURL = getPrettyURL(
-    sourceFileName,
-    sourceLineNumber,
-    sourceColumnNumber,
-    fileName,
-    lineNumber,
-    columnNumber,
-    compiled
-  );
-
-  let needsHidden = false;
-  const isInternalUrl = isInternalFile(sourceFileName, fileName);
-  const isThrownIntentionally = !isBultinErrorName(errorName);
-  const shouldCollapse =
-    isInternalUrl && (isThrownIntentionally || omits.hasReachedAppCode);
-
-  if (!isInternalUrl) {
-    omits.hasReachedAppCode = true;
-  }
-
-  if (shouldCollapse) {
-    ++omits.value;
-    needsHidden = true;
-  }
-
-  let collapseElement = null;
-  if (!shouldCollapse || lastElement) {
-    if (omits.value > 0) {
-      const capV = omits.value;
-      const omittedFrames = getGroupToggle(document, capV, omitBundle);
-      window.requestAnimationFrame(() => {
-        insertBeforeBundle(
-          document,
-          parentContainer,
-          capV,
-          omitBundle,
-          omittedFrames
-        );
-      });
-      if (lastElement && shouldCollapse) {
-        collapseElement = omittedFrames;
-      } else {
-        parentContainer.appendChild(omittedFrames);
-      }
-      ++omits.bundle;
-    }
-    omits.value = 0;
-  }
-
-  let onSourceClick = null;
-  if (sourceFileName) {
-    // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1"
-    const isInternalWebpackBootstrapCode =
-      sourceFileName.trim().indexOf(' ') !== -1;
-    if (!isInternalWebpackBootstrapCode) {
-      onSourceClick = () => {
-        // Keep this in sync with react-error-overlay/middleware.js
-        fetch(
-          '/__open-stack-frame-in-editor?fileName=' +
-            window.encodeURIComponent(sourceFileName) +
-            '&lineNumber=' +
-            window.encodeURIComponent(sourceLineNumber || 1)
-        ).then(() => {}, () => {});
-      };
-    }
-  }
-
-  const elem = frameDiv(
-    document,
-    functionName,
-    prettyURL,
-    shouldCollapse,
-    onSourceClick
-  );
-  if (needsHidden) {
-    applyStyles(elem, hiddenStyle);
-    elem.setAttribute('name', 'bundle-' + omitBundle);
-  }
-
-  let hasSource = false;
-  if (!shouldCollapse) {
-    if (
-      compiled &&
-      scriptLines &&
-      scriptLines.length !== 0 &&
-      lineNumber != null
-    ) {
-      elem.appendChild(
-        createCode(
-          document,
-          scriptLines,
-          lineNumber,
-          columnNumber,
-          contextSize,
-          critical,
-          onSourceClick
-        )
-      );
-      hasSource = true;
-    } else if (
-      !compiled &&
-      sourceLines &&
-      sourceLines.length !== 0 &&
-      sourceLineNumber != null
-    ) {
-      elem.appendChild(
-        createCode(
-          document,
-          sourceLines,
-          sourceLineNumber,
-          sourceColumnNumber,
-          contextSize,
-          critical,
-          onSourceClick
-        )
-      );
-      hasSource = true;
-    }
-  }
-
-  return { elem: elem, hasSource: hasSource, collapseElement: collapseElement };
-}
-
-export { createFrame };
diff --git a/packages/react-error-overlay/src/components/frames.js b/packages/react-error-overlay/src/components/frames.js
deleted file mode 100644
index 8bd50509295..00000000000
--- a/packages/react-error-overlay/src/components/frames.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import type { StackFrame } from '../utils/stack-frame';
-import { applyStyles } from '../utils/dom/css';
-import { traceStyle, toggleStyle } from '../styles';
-import { enableTabClick } from '../utils/dom/enableTabClick';
-import { createFrame } from './frame';
-
-type OmitsObject = {
-  value: number,
-  bundle: number,
-  hasReachedAppCode: boolean,
-};
-type FrameSetting = { compiled: boolean };
-export type { OmitsObject, FrameSetting };
-
-function createFrameWrapper(
-  document: Document,
-  parent: HTMLDivElement,
-  factory,
-  lIndex: number,
-  frameSettings: FrameSetting[],
-  contextSize: number
-) {
-  const fac = factory();
-  if (fac == null) {
-    return;
-  }
-  const { hasSource, elem, collapseElement } = fac;
-
-  const elemWrapper = document.createElement('div');
-  elemWrapper.appendChild(elem);
-
-  if (hasSource) {
-    const compiledDiv = document.createElement('div');
-    enableTabClick(compiledDiv);
-    applyStyles(compiledDiv, toggleStyle);
-
-    const o = frameSettings[lIndex];
-    const compiledText = document.createTextNode(
-      'View ' + (o && o.compiled ? 'source' : 'compiled')
-    );
-    compiledDiv.addEventListener('click', function() {
-      if (o) {
-        o.compiled = !o.compiled;
-      }
-
-      const next = createFrameWrapper(
-        document,
-        parent,
-        factory,
-        lIndex,
-        frameSettings,
-        contextSize
-      );
-      if (next != null) {
-        parent.insertBefore(next, elemWrapper);
-        parent.removeChild(elemWrapper);
-      }
-    });
-    compiledDiv.appendChild(compiledText);
-    elemWrapper.appendChild(compiledDiv);
-  }
-
-  if (collapseElement != null) {
-    elemWrapper.appendChild(collapseElement);
-  }
-
-  return elemWrapper;
-}
-
-function createFrames(
-  document: Document,
-  resolvedFrames: StackFrame[],
-  frameSettings: FrameSetting[],
-  contextSize: number,
-  errorName: ?string
-) {
-  if (resolvedFrames.length !== frameSettings.length) {
-    throw new Error(
-      'You must give a frame settings array of identical length to resolved frames.'
-    );
-  }
-  const trace = document.createElement('div');
-  applyStyles(trace, traceStyle);
-
-  let index = 0;
-  let critical = true;
-  const omits: OmitsObject = { value: 0, bundle: 1, hasReachedAppCode: false };
-  resolvedFrames.forEach(function(frame) {
-    const lIndex = index++;
-    const elem = createFrameWrapper(
-      document,
-      trace,
-      createFrame.bind(
-        undefined,
-        document,
-        frameSettings[lIndex],
-        frame,
-        contextSize,
-        critical,
-        omits,
-        omits.bundle,
-        trace,
-        index === resolvedFrames.length,
-        errorName
-      ),
-      lIndex,
-      frameSettings,
-      contextSize
-    );
-    if (elem == null) {
-      return;
-    }
-    critical = false;
-    trace.appendChild(elem);
-  });
-  //TODO: fix this
-  omits.value = 0;
-
-  return trace;
-}
-
-export { createFrames };
diff --git a/packages/react-error-overlay/src/components/overlay.js b/packages/react-error-overlay/src/components/overlay.js
deleted file mode 100644
index 69acf9ad43f..00000000000
--- a/packages/react-error-overlay/src/components/overlay.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import { applyStyles } from '../utils/dom/css';
-import { containerStyle, overlayStyle, headerStyle } from '../styles';
-import { createClose } from './close';
-import { createFrames } from './frames';
-import { createFooter } from './footer';
-import type { CloseCallback } from './close';
-import type { StackFrame } from '../utils/stack-frame';
-import { updateAdditional } from './additional';
-import type { FrameSetting } from './frames';
-import type { SwitchCallback } from './additional';
-
-function createOverlay(
-  document: Document,
-  name: ?string,
-  message: string,
-  frames: StackFrame[],
-  contextSize: number,
-  currentError: number,
-  totalErrors: number,
-  switchCallback: SwitchCallback,
-  closeCallback: CloseCallback
-): {
-  overlay: HTMLDivElement,
-  additional: HTMLDivElement,
-} {
-  const frameSettings: FrameSetting[] = frames.map(() => ({ compiled: false }));
-  // Create overlay
-  const overlay = document.createElement('div');
-  applyStyles(overlay, overlayStyle);
-
-  // Create container
-  const container = document.createElement('div');
-  applyStyles(container, containerStyle);
-  overlay.appendChild(container);
-  container.appendChild(createClose(document, closeCallback));
-
-  // Create "Errors X of Y" in case of multiple errors
-  const additional = document.createElement('div');
-  updateAdditional(
-    document,
-    additional,
-    currentError,
-    totalErrors,
-    switchCallback
-  );
-  container.appendChild(additional);
-
-  // Create header
-  const header = document.createElement('div');
-  applyStyles(header, headerStyle);
-
-  // Make message prettier
-  let finalMessage =
-    message.match(/^\w*:/) || !name ? message : name + ': ' + message;
-
-  finalMessage = finalMessage
-    // TODO: maybe remove this prefix from fbjs?
-    // It's just scaring people
-    .replace(/^Invariant Violation:\s*/, '')
-    // This is not helpful either:
-    .replace(/^Warning:\s*/, '')
-    // Break the actionable part to the next line.
-    // AFAIK React 16+ should already do this.
-    .replace(' Check the render method', '\n\nCheck the render method')
-    .replace(' Check your code at', '\n\nCheck your code at');
-
-  // Put it in the DOM
-  header.appendChild(document.createTextNode(finalMessage));
-  container.appendChild(header);
-
-  // Create trace
-  container.appendChild(
-    createFrames(document, frames, frameSettings, contextSize, name)
-  );
-
-  // Show message
-  container.appendChild(createFooter(document));
-
-  return {
-    overlay,
-    additional,
-  };
-}
-
-export { createOverlay };
diff --git a/packages/react-error-overlay/src/containers/CompileErrorContainer.js b/packages/react-error-overlay/src/containers/CompileErrorContainer.js
new file mode 100644
index 00000000000..bd193eb50b4
--- /dev/null
+++ b/packages/react-error-overlay/src/containers/CompileErrorContainer.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React, { PureComponent } from 'react';
+import Overlay from '../components/Overlay';
+import Footer from '../components/Footer';
+import Header from '../components/Header';
+import CodeBlock from '../components/CodeBlock';
+import generateAnsiHTML from '../utils/generateAnsiHTML';
+
+class CompileErrorContainer extends PureComponent {
+  render() {
+    const { error } = this.props;
+    return (
+      <Overlay>
+        <Header headerText="Failed to compile" />
+        <CodeBlock main={true} codeHTML={generateAnsiHTML(error)} />
+        <Footer line1="This error occurred during the build time and cannot be dismissed." />
+      </Overlay>
+    );
+  }
+}
+
+export default CompileErrorContainer;
diff --git a/packages/react-error-overlay/src/containers/RuntimeError.js b/packages/react-error-overlay/src/containers/RuntimeError.js
new file mode 100644
index 00000000000..c64824137d2
--- /dev/null
+++ b/packages/react-error-overlay/src/containers/RuntimeError.js
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React from 'react';
+import Header from '../components/Header';
+import StackTrace from './StackTrace';
+import type { StackFrame } from '../utils/stack-frame';
+
+const wrapperStyle = {
+  display: 'flex',
+  flexDirection: 'column',
+};
+
+type ErrorRecord = {|
+  error: Error,
+  unhandledRejection: boolean,
+  contextSize: number,
+  stackFrames: StackFrame[],
+|};
+
+type Props = {|
+  errorRecord: ErrorRecord,
+  launchEditorEndpoint: ?string,
+|};
+
+function RuntimeError({ errorRecord, launchEditorEndpoint }: Props) {
+  const { error, unhandledRejection, contextSize, stackFrames } = errorRecord;
+  const errorName = unhandledRejection
+    ? 'Unhandled Rejection (' + error.name + ')'
+    : error.name;
+
+  // Make header prettier
+  const message = error.message;
+  let headerText =
+    message.match(/^\w*:/) || !errorName ? message : errorName + ': ' + message;
+
+  headerText = headerText
+    // TODO: maybe remove this prefix from fbjs?
+    // It's just scaring people
+    .replace(/^Invariant Violation:\s*/, '')
+    // This is not helpful either:
+    .replace(/^Warning:\s*/, '')
+    // Break the actionable part to the next line.
+    // AFAIK React 16+ should already do this.
+    .replace(' Check the render method', '\n\nCheck the render method')
+    .replace(' Check your code at', '\n\nCheck your code at');
+
+  return (
+    <div style={wrapperStyle}>
+      <Header headerText={headerText} />
+      <StackTrace
+        stackFrames={stackFrames}
+        errorName={errorName}
+        contextSize={contextSize}
+        launchEditorEndpoint={launchEditorEndpoint}
+      />
+    </div>
+  );
+}
+
+export default RuntimeError;
diff --git a/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js b/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js
new file mode 100644
index 00000000000..c84adb19492
--- /dev/null
+++ b/packages/react-error-overlay/src/containers/RuntimeErrorContainer.js
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React, { PureComponent } from 'react';
+import Overlay from '../components/Overlay';
+import CloseButton from '../components/CloseButton';
+import NavigationBar from '../components/NavigationBar';
+import RuntimeError from './RuntimeError';
+import Footer from '../components/Footer';
+
+class RuntimeErrorContainer extends PureComponent {
+  state = {
+    currentIndex: 0,
+  };
+
+  previous = () => {
+    this.setState((state, props) => ({
+      currentIndex:
+        state.currentIndex > 0
+          ? state.currentIndex - 1
+          : props.errorRecords.length - 1,
+    }));
+  };
+
+  next = () => {
+    this.setState((state, props) => ({
+      currentIndex:
+        state.currentIndex < props.errorRecords.length - 1
+          ? state.currentIndex + 1
+          : 0,
+    }));
+  };
+
+  shortcutHandler = (key: string) => {
+    if (key === 'Escape') {
+      this.props.close();
+    } else if (key === 'ArrowLeft') {
+      this.previous();
+    } else if (key === 'ArrowRight') {
+      this.next();
+    }
+  };
+
+  render() {
+    const { errorRecords, close } = this.props;
+    const totalErrors = errorRecords.length;
+    return (
+      <Overlay shortcutHandler={this.shortcutHandler}>
+        <CloseButton close={close} />
+        {totalErrors > 1 &&
+          <NavigationBar
+            currentError={this.state.currentIndex + 1}
+            totalErrors={totalErrors}
+            previous={this.previous}
+            next={this.next}
+          />}
+        <RuntimeError
+          errorRecord={errorRecords[this.state.currentIndex]}
+          launchEditorEndpoint={this.props.launchEditorEndpoint}
+        />
+        <Footer
+          line1="This screen is visible only in development. It will not appear if the app crashes in production."
+          line2="Open your browser’s developer console to further inspect this error."
+        />
+      </Overlay>
+    );
+  }
+}
+
+export default RuntimeErrorContainer;
diff --git a/packages/react-error-overlay/src/containers/StackFrame.js b/packages/react-error-overlay/src/containers/StackFrame.js
new file mode 100644
index 00000000000..c95ce003f49
--- /dev/null
+++ b/packages/react-error-overlay/src/containers/StackFrame.js
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React, { Component } from 'react';
+import CodeBlock from './StackFrameCodeBlock';
+import { getPrettyURL } from '../utils/getPrettyURL';
+import { darkGray } from '../styles';
+
+const linkStyle = {
+  fontSize: '0.9em',
+  marginBottom: '0.9em',
+};
+
+const anchorStyle = {
+  textDecoration: 'none',
+  color: darkGray,
+  cursor: 'pointer',
+};
+
+const codeAnchorStyle = {
+  cursor: 'pointer',
+};
+
+const toggleStyle = {
+  marginBottom: '1.5em',
+  color: darkGray,
+  cursor: 'pointer',
+  border: 'none',
+  display: 'block',
+  width: '100%',
+  textAlign: 'left',
+  background: '#fff',
+  fontFamily: 'Consolas, Menlo, monospace',
+  fontSize: '1em',
+  padding: '0px',
+  lineHeight: '1.5',
+};
+
+class StackFrame extends Component {
+  state = {
+    compiled: false,
+  };
+
+  toggleCompiled = () => {
+    this.setState(state => ({
+      compiled: !state.compiled,
+    }));
+  };
+
+  canOpenInEditor() {
+    if (!this.props.launchEditorEndpoint) {
+      return;
+    }
+    const { _originalFileName: sourceFileName } = this.props.frame;
+    // Unknown file
+    if (!sourceFileName) {
+      return false;
+    }
+    // e.g. "/path-to-my-app/webpack/bootstrap eaddeb46b67d75e4dfc1"
+    const isInternalWebpackBootstrapCode =
+      sourceFileName.trim().indexOf(' ') !== -1;
+    if (isInternalWebpackBootstrapCode) {
+      return false;
+    }
+    // Code is in a real file
+    return true;
+  }
+
+  openInEditor = () => {
+    if (!this.canOpenInEditor()) {
+      return;
+    }
+    const {
+      _originalFileName: sourceFileName,
+      _originalLineNumber: sourceLineNumber,
+    } = this.props.frame;
+    // Keep this in sync with react-error-overlay/middleware.js
+    fetch(
+      `${this.props.launchEditorEndpoint}?fileName=` +
+        window.encodeURIComponent(sourceFileName) +
+        '&lineNumber=' +
+        window.encodeURIComponent(sourceLineNumber || 1)
+    ).then(() => {}, () => {});
+  };
+
+  onKeyDown = (e: SyntheticKeyboardEvent) => {
+    if (e.key === 'Enter') {
+      this.openInEditor();
+    }
+  };
+
+  render() {
+    const { frame, contextSize, critical, showCode } = this.props;
+    const {
+      fileName,
+      lineNumber,
+      columnNumber,
+      _scriptCode: scriptLines,
+      _originalFileName: sourceFileName,
+      _originalLineNumber: sourceLineNumber,
+      _originalColumnNumber: sourceColumnNumber,
+      _originalScriptCode: sourceLines,
+    } = frame;
+    const functionName = frame.getFunctionName();
+
+    const compiled = this.state.compiled;
+    const url = getPrettyURL(
+      sourceFileName,
+      sourceLineNumber,
+      sourceColumnNumber,
+      fileName,
+      lineNumber,
+      columnNumber,
+      compiled
+    );
+
+    let codeBlockProps = null;
+    if (showCode) {
+      if (
+        compiled &&
+        scriptLines &&
+        scriptLines.length !== 0 &&
+        lineNumber != null
+      ) {
+        codeBlockProps = {
+          lines: scriptLines,
+          lineNum: lineNumber,
+          columnNum: columnNumber,
+          contextSize,
+          main: critical,
+        };
+      } else if (
+        !compiled &&
+        sourceLines &&
+        sourceLines.length !== 0 &&
+        sourceLineNumber != null
+      ) {
+        codeBlockProps = {
+          lines: sourceLines,
+          lineNum: sourceLineNumber,
+          columnNum: sourceColumnNumber,
+          contextSize,
+          main: critical,
+        };
+      }
+    }
+
+    const canOpenInEditor = this.canOpenInEditor();
+    return (
+      <div>
+        <div>
+          {functionName}
+        </div>
+        <div style={linkStyle}>
+          <a
+            style={canOpenInEditor ? anchorStyle : null}
+            onClick={canOpenInEditor ? this.openInEditor : null}
+            onKeyDown={canOpenInEditor ? this.onKeyDown : null}
+            tabIndex={canOpenInEditor ? '0' : null}
+          >
+            {url}
+          </a>
+        </div>
+        {codeBlockProps &&
+          <span>
+            <a
+              onClick={canOpenInEditor ? this.openInEditor : null}
+              style={canOpenInEditor ? codeAnchorStyle : null}
+            >
+              <CodeBlock {...codeBlockProps} />
+            </a>
+            <button style={toggleStyle} onClick={this.toggleCompiled}>
+              {'View ' + (compiled ? 'source' : 'compiled')}
+            </button>
+          </span>}
+      </div>
+    );
+  }
+}
+
+export default StackFrame;
diff --git a/packages/react-error-overlay/src/components/code.js b/packages/react-error-overlay/src/containers/StackFrameCodeBlock.js
similarity index 71%
rename from packages/react-error-overlay/src/components/code.js
rename to packages/react-error-overlay/src/containers/StackFrameCodeBlock.js
index 580fe3b1be5..2ed685cff49 100644
--- a/packages/react-error-overlay/src/components/code.js
+++ b/packages/react-error-overlay/src/containers/StackFrameCodeBlock.js
@@ -8,33 +8,29 @@
  */
 
 /* @flow */
-import type { ScriptLine } from '../utils/stack-frame';
+import React from 'react';
+import CodeBlock from '../components/CodeBlock';
 import { applyStyles } from '../utils/dom/css';
 import { absolutifyCaret } from '../utils/dom/absolutifyCaret';
-import {
-  codeStyle,
-  primaryErrorStyle,
-  primaryPreStyle,
-  secondaryErrorStyle,
-  secondaryPreStyle,
-} from '../styles';
-
-import generateAnsiHtml from 'react-dev-utils/ansiHTML';
+import type { ScriptLine } from '../utils/stack-frame';
+import { primaryErrorStyle, secondaryErrorStyle } from '../styles';
+import generateAnsiHTML from '../utils/generateAnsiHTML';
 
 import codeFrame from 'babel-code-frame';
 
-function createCode(
-  document: Document,
-  sourceLines: ScriptLine[],
+type StackFrameCodeBlockPropsType = {|
+  lines: ScriptLine[],
   lineNum: number,
-  columnNum: number | null,
+  columnNum: number,
   contextSize: number,
   main: boolean,
-  onSourceClick: ?Function
-) {
+|};
+
+function StackFrameCodeBlock(props: StackFrameCodeBlockPropsType) {
+  const { lines, lineNum, columnNum, contextSize, main } = props;
   const sourceCode = [];
   let whiteSpace = Infinity;
-  sourceLines.forEach(function(e) {
+  lines.forEach(function(e) {
     const { content: text } = e;
     const m = text.match(/^\s*/);
     if (text === '') {
@@ -46,7 +42,7 @@ function createCode(
       whiteSpace = 0;
     }
   });
-  sourceLines.forEach(function(e) {
+  lines.forEach(function(e) {
     let { content: text } = e;
     const { lineNumber: line } = e;
 
@@ -65,11 +61,10 @@ function createCode(
       linesBelow: contextSize,
     }
   );
-  const htmlHighlight = generateAnsiHtml(ansiHighlight);
+  const htmlHighlight = generateAnsiHTML(ansiHighlight);
   const code = document.createElement('code');
   code.innerHTML = htmlHighlight;
   absolutifyCaret(code);
-  applyStyles(code, codeStyle);
 
   const ccn = code.childNodes;
   // eslint-disable-next-line
@@ -91,19 +86,8 @@ function createCode(
       break oLoop;
     }
   }
-  const pre = document.createElement('pre');
-  applyStyles(pre, main ? primaryPreStyle : secondaryPreStyle);
-  pre.appendChild(code);
-
-  if (typeof onSourceClick === 'function') {
-    let handler = onSourceClick;
-    pre.style.cursor = 'pointer';
-    pre.addEventListener('click', function() {
-      handler();
-    });
-  }
 
-  return pre;
+  return <CodeBlock main={main} codeHTML={code.innerHTML} />;
 }
 
-export { createCode };
+export default StackFrameCodeBlock;
diff --git a/packages/react-error-overlay/src/containers/StackTrace.js b/packages/react-error-overlay/src/containers/StackTrace.js
new file mode 100644
index 00000000000..4cb20bce128
--- /dev/null
+++ b/packages/react-error-overlay/src/containers/StackTrace.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import React, { Component } from 'react';
+import StackFrame from './StackFrame';
+import Collapsible from '../components/Collapsible';
+import { isInternalFile } from '../utils/isInternalFile';
+import { isBultinErrorName } from '../utils/isBultinErrorName';
+
+const traceStyle = {
+  fontSize: '1em',
+  flex: '0 1 auto',
+  minHeight: '0px',
+  overflow: 'auto',
+};
+
+class StackTrace extends Component {
+  renderFrames() {
+    const {
+      stackFrames,
+      errorName,
+      contextSize,
+      launchEditorEndpoint,
+    } = this.props;
+    const renderedFrames = [];
+    let hasReachedAppCode = false,
+      currentBundle = [],
+      bundleCount = 0;
+
+    stackFrames.forEach((frame, index) => {
+      const { fileName, _originalFileName: sourceFileName } = frame;
+      const isInternalUrl = isInternalFile(sourceFileName, fileName);
+      const isThrownIntentionally = !isBultinErrorName(errorName);
+      const shouldCollapse =
+        isInternalUrl && (isThrownIntentionally || hasReachedAppCode);
+
+      if (!isInternalUrl) {
+        hasReachedAppCode = true;
+      }
+
+      const frameEle = (
+        <StackFrame
+          key={'frame-' + index}
+          frame={frame}
+          contextSize={contextSize}
+          critical={index === 0}
+          showCode={!shouldCollapse}
+          launchEditorEndpoint={launchEditorEndpoint}
+        />
+      );
+      const lastElement = index === stackFrames.length - 1;
+
+      if (shouldCollapse) {
+        currentBundle.push(frameEle);
+      }
+
+      if (!shouldCollapse || lastElement) {
+        if (currentBundle.length === 1) {
+          renderedFrames.push(currentBundle[0]);
+        } else if (currentBundle.length > 1) {
+          bundleCount++;
+          renderedFrames.push(
+            <Collapsible key={'bundle-' + bundleCount}>
+              {currentBundle}
+            </Collapsible>
+          );
+        }
+        currentBundle = [];
+      }
+
+      if (!shouldCollapse) {
+        renderedFrames.push(frameEle);
+      }
+    });
+
+    return renderedFrames;
+  }
+
+  render() {
+    return (
+      <div style={traceStyle}>
+        {this.renderFrames()}
+      </div>
+    );
+  }
+}
+
+export default StackTrace;
diff --git a/packages/react-error-overlay/src/effects/shortcuts.js b/packages/react-error-overlay/src/effects/shortcuts.js
deleted file mode 100644
index bf8fd6d5eb7..00000000000
--- a/packages/react-error-overlay/src/effects/shortcuts.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-const SHORTCUT_ESCAPE = 'SHORTCUT_ESCAPE',
-  SHORTCUT_LEFT = 'SHORTCUT_LEFT',
-  SHORTCUT_RIGHT = 'SHORTCUT_RIGHT';
-
-let boundKeyHandler = null;
-
-type ShortcutCallback = (type: string) => void;
-
-function keyHandler(callback: ShortcutCallback, e: KeyboardEvent) {
-  const { key, keyCode, which } = e;
-  if (key === 'Escape' || keyCode === 27 || which === 27) {
-    callback(SHORTCUT_ESCAPE);
-  } else if (key === 'ArrowLeft' || keyCode === 37 || which === 37) {
-    callback(SHORTCUT_LEFT);
-  } else if (key === 'ArrowRight' || keyCode === 39 || which === 39) {
-    callback(SHORTCUT_RIGHT);
-  }
-}
-
-function registerShortcuts(target: EventTarget, callback: ShortcutCallback) {
-  if (boundKeyHandler !== null) {
-    return;
-  }
-  boundKeyHandler = keyHandler.bind(undefined, callback);
-  target.addEventListener('keydown', boundKeyHandler);
-}
-
-function unregisterShortcuts(target: EventTarget) {
-  if (boundKeyHandler === null) {
-    return;
-  }
-  target.removeEventListener('keydown', boundKeyHandler);
-  boundKeyHandler = null;
-}
-
-export {
-  SHORTCUT_ESCAPE,
-  SHORTCUT_LEFT,
-  SHORTCUT_RIGHT,
-  registerShortcuts as register,
-  unregisterShortcuts as unregister,
-  keyHandler as handler,
-};
diff --git a/packages/react-error-overlay/src/index.js b/packages/react-error-overlay/src/index.js
index 4f3b2316727..ff4f1c71404 100644
--- a/packages/react-error-overlay/src/index.js
+++ b/packages/react-error-overlay/src/index.js
@@ -8,11 +8,158 @@
  */
 
 /* @flow */
-import { inject, uninject } from './overlay';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import CompileErrorContainer from './containers/CompileErrorContainer';
+import RuntimeErrorContainer from './containers/RuntimeErrorContainer';
+import { listenToRuntimeErrors } from './listenToRuntimeErrors';
+import { iframeStyle, overlayStyle } from './styles';
+import { applyStyles } from './utils/dom/css';
 
-inject();
-if (module.hot && typeof module.hot.dispose === 'function') {
-  module.hot.dispose(function() {
-    uninject();
+import type { ErrorRecord } from './listenToRuntimeErrors';
+
+type RuntimeReportingOptions = {|
+  onError: () => void,
+  launchEditorEndpoint: string,
+|};
+
+let iframe: null | HTMLIFrameElement = null;
+let isLoadingIframe: boolean = false;
+
+let renderedElement: null | React.Element<any> = null;
+let currentBuildError: null | string = null;
+let currentRuntimeErrorRecords: Array<ErrorRecord> = [];
+let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null;
+let stopListeningToRuntimeErrors: null | (() => void) = null;
+
+export function reportBuildError(error: string) {
+  currentBuildError = error;
+  update();
+}
+
+export function dismissBuildError() {
+  currentBuildError = null;
+  update();
+}
+
+export function startReportingRuntimeErrors(options: RuntimeReportingOptions) {
+  if (stopListeningToRuntimeErrors !== null) {
+    throw new Error('Already listening');
+  }
+  currentRuntimeErrorOptions = options;
+  listenToRuntimeErrors(errorRecord => {
+    try {
+      if (typeof options.onError === 'function') {
+        options.onError.call(null);
+      }
+    } finally {
+      handleRuntimeError(errorRecord);
+    }
   });
 }
+
+function handleRuntimeError(errorRecord) {
+  if (
+    currentRuntimeErrorRecords.some(({ error }) => error === errorRecord.error)
+  ) {
+    // Deduplicate identical errors.
+    // This fixes https://github.com/facebookincubator/create-react-app/issues/3011.
+    return;
+  }
+  currentRuntimeErrorRecords = currentRuntimeErrorRecords.concat([errorRecord]);
+  update();
+}
+
+function dismissRuntimeErrors() {
+  currentRuntimeErrorRecords = [];
+  update();
+}
+
+export function stopReportingRuntimeErrors() {
+  if (stopListeningToRuntimeErrors === null) {
+    throw new Error('Not currently listening');
+  }
+  currentRuntimeErrorOptions = null;
+  try {
+    stopListeningToRuntimeErrors();
+  } finally {
+    stopListeningToRuntimeErrors = null;
+  }
+}
+
+function update() {
+  renderedElement = render();
+  // Loading iframe can be either sync or async depending on the browser.
+  if (isLoadingIframe) {
+    // Iframe is loading.
+    // First render will happen soon--don't need to do anything.
+    return;
+  }
+  if (iframe) {
+    // Iframe has already loaded.
+    // Just update it.
+    updateIframeContent();
+    return;
+  }
+  // We need to schedule the first render.
+  isLoadingIframe = true;
+  const loadingIframe = window.document.createElement('iframe');
+  applyStyles(loadingIframe, iframeStyle);
+  loadingIframe.onload = function() {
+    const iframeDocument = loadingIframe.contentDocument;
+    if (iframeDocument != null && iframeDocument.body != null) {
+      iframeDocument.body.style.margin = '0';
+      // Keep popup within body boundaries for iOS Safari
+      iframeDocument.body.style['max-width'] = '100vw';
+      const iframeRoot = iframeDocument.createElement('div');
+      applyStyles(iframeRoot, overlayStyle);
+      iframeDocument.body.appendChild(iframeRoot);
+
+      // Ready! Now we can update the UI.
+      iframe = loadingIframe;
+      isLoadingIframe = false;
+      updateIframeContent();
+    }
+  };
+  const appDocument = window.document;
+  appDocument.body.appendChild(loadingIframe);
+}
+
+function render() {
+  if (currentBuildError) {
+    return <CompileErrorContainer error={currentBuildError} />;
+  }
+  if (currentRuntimeErrorRecords.length > 0) {
+    if (!currentRuntimeErrorOptions) {
+      throw new Error('Expected options to be injected.');
+    }
+    return (
+      <RuntimeErrorContainer
+        errorRecords={currentRuntimeErrorRecords}
+        close={dismissRuntimeErrors}
+        launchEditorEndpoint={currentRuntimeErrorOptions.launchEditorEndpoint}
+      />
+    );
+  }
+  return null;
+}
+
+function updateIframeContent() {
+  if (iframe === null) {
+    throw new Error('Iframe has not been created yet.');
+  }
+  const iframeBody = iframe.contentDocument.body;
+  if (!iframeBody) {
+    throw new Error('Expected iframe to have a body.');
+  }
+  const iframeRoot = iframeBody.firstChild;
+  if (renderedElement === null) {
+    // Destroy iframe and force it to be recreated on next error
+    window.document.body.removeChild(iframe);
+    ReactDOM.unmountComponentAtNode(iframeRoot);
+    iframe = null;
+    return;
+  }
+  // Update the overlay
+  ReactDOM.render(renderedElement, iframeRoot);
+}
diff --git a/packages/react-error-overlay/src/listenToRuntimeErrors.js b/packages/react-error-overlay/src/listenToRuntimeErrors.js
new file mode 100644
index 00000000000..45c43fa5d40
--- /dev/null
+++ b/packages/react-error-overlay/src/listenToRuntimeErrors.js
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+import {
+  register as registerError,
+  unregister as unregisterError,
+} from './effects/unhandledError';
+import {
+  register as registerPromise,
+  unregister as unregisterPromise,
+} from './effects/unhandledRejection';
+import {
+  register as registerStackTraceLimit,
+  unregister as unregisterStackTraceLimit,
+} from './effects/stackTraceLimit';
+import {
+  permanentRegister as permanentRegisterConsole,
+  registerReactStack,
+  unregisterReactStack,
+} from './effects/proxyConsole';
+import { massage as massageWarning } from './utils/warnings';
+import getStackFrames from './utils/getStackFrames';
+
+import type { StackFrame } from './utils/stack-frame';
+
+const CONTEXT_SIZE: number = 3;
+
+export type ErrorRecord = {|
+  error: Error,
+  unhandledRejection: boolean,
+  contextSize: number,
+  stackFrames: StackFrame[],
+|};
+
+export function listenToRuntimeErrors(crash: ErrorRecord => void) {
+  function crashWithFrames(error: Error, unhandledRejection = false) {
+    getStackFrames(error, unhandledRejection, CONTEXT_SIZE)
+      .then(stackFrames => {
+        if (stackFrames == null) {
+          return;
+        }
+        crash({
+          error,
+          unhandledRejection,
+          contextSize: CONTEXT_SIZE,
+          stackFrames,
+        });
+      })
+      .catch(e => {
+        console.log('Could not get the stack frames of error:', e);
+      });
+  }
+  registerError(window, error => crashWithFrames(error, false));
+  registerPromise(window, error => crashWithFrames(error, true));
+  registerStackTraceLimit();
+  registerReactStack();
+  permanentRegisterConsole('error', (warning, stack) => {
+    const data = massageWarning(warning, stack);
+    crashWithFrames(
+      // $FlowFixMe
+      {
+        message: data.message,
+        stack: data.stack,
+        __unmap_source: '/static/js/bundle.js',
+      },
+      false
+    );
+  });
+
+  return function stopListening() {
+    unregisterStackTraceLimit();
+    unregisterPromise(window);
+    unregisterError(window);
+    unregisterReactStack();
+  };
+}
diff --git a/packages/react-error-overlay/src/overlay.js b/packages/react-error-overlay/src/overlay.js
deleted file mode 100644
index 181cb02716d..00000000000
--- a/packages/react-error-overlay/src/overlay.js
+++ /dev/null
@@ -1,235 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-import {
-  register as registerError,
-  unregister as unregisterError,
-} from './effects/unhandledError';
-import {
-  register as registerPromise,
-  unregister as unregisterPromise,
-} from './effects/unhandledRejection';
-import {
-  register as registerShortcuts,
-  unregister as unregisterShortcuts,
-  handler as keyEventHandler,
-  SHORTCUT_ESCAPE,
-  SHORTCUT_LEFT,
-  SHORTCUT_RIGHT,
-} from './effects/shortcuts';
-import {
-  register as registerStackTraceLimit,
-  unregister as unregisterStackTraceLimit,
-} from './effects/stackTraceLimit';
-import {
-  permanentRegister as permanentRegisterConsole,
-  registerReactStack,
-  unregisterReactStack,
-} from './effects/proxyConsole';
-import { massage as massageWarning } from './utils/warnings';
-
-import {
-  consume as consumeError,
-  getErrorRecord,
-  drain as drainErrors,
-} from './utils/errorRegister';
-import type { ErrorRecordReference } from './utils/errorRegister';
-
-import type { StackFrame } from './utils/stack-frame';
-import { iframeStyle } from './styles';
-import { applyStyles } from './utils/dom/css';
-import { createOverlay } from './components/overlay';
-import { updateAdditional } from './components/additional';
-
-const CONTEXT_SIZE: number = 3;
-let iframeReference: HTMLIFrameElement | null = null;
-let additionalReference = null;
-let errorReferences: ErrorRecordReference[] = [];
-let currReferenceIndex: number = -1;
-
-function render(name: ?string, message: string, resolvedFrames: StackFrame[]) {
-  disposeCurrentView();
-
-  const iframe = window.document.createElement('iframe');
-  applyStyles(iframe, iframeStyle);
-  iframeReference = iframe;
-  iframe.onload = () => {
-    if (iframeReference == null) {
-      return;
-    }
-    const w = iframeReference.contentWindow;
-    const document = iframeReference.contentDocument;
-
-    const { overlay, additional } = createOverlay(
-      document,
-      name,
-      message,
-      resolvedFrames,
-      CONTEXT_SIZE,
-      currReferenceIndex + 1,
-      errorReferences.length,
-      offset => {
-        switchError(offset);
-      },
-      () => {
-        unmount();
-      }
-    );
-    if (w != null) {
-      w.onkeydown = event => {
-        keyEventHandler(type => shortcutHandler(type), event);
-      };
-    }
-    if (document.body != null) {
-      document.body.style.margin = '0';
-      // Keep popup within body boundaries for iOS Safari
-      // $FlowFixMe
-      document.body.style['max-width'] = '100vw';
-
-      (document.body: any).appendChild(overlay);
-    }
-    additionalReference = additional;
-  };
-  window.document.body.appendChild(iframe);
-}
-
-function renderErrorByIndex(index: number) {
-  currReferenceIndex = index;
-
-  const { error, unhandledRejection, enhancedFrames } = getErrorRecord(
-    errorReferences[index]
-  );
-
-  if (unhandledRejection) {
-    render(
-      'Unhandled Rejection (' + error.name + ')',
-      error.message,
-      enhancedFrames
-    );
-  } else {
-    render(error.name, error.message, enhancedFrames);
-  }
-}
-
-function switchError(offset) {
-  if (errorReferences.length === 0) {
-    return;
-  }
-
-  let nextView = currReferenceIndex + offset;
-
-  if (nextView < 0) {
-    nextView = errorReferences.length - 1;
-  } else if (nextView >= errorReferences.length) {
-    nextView = 0;
-  }
-
-  renderErrorByIndex(nextView);
-}
-
-function disposeCurrentView() {
-  if (iframeReference === null) {
-    return;
-  }
-  window.document.body.removeChild(iframeReference);
-  iframeReference = null;
-  additionalReference = null;
-}
-
-function unmount() {
-  disposeCurrentView();
-  drainErrors();
-  errorReferences = [];
-  currReferenceIndex = -1;
-}
-
-function crash(error: Error, unhandledRejection = false) {
-  if (module.hot && typeof module.hot.decline === 'function') {
-    module.hot.decline();
-  }
-  consumeError(error, unhandledRejection, CONTEXT_SIZE)
-    .then(ref => {
-      if (ref == null) {
-        return;
-      }
-      errorReferences.push(ref);
-      if (iframeReference !== null && additionalReference !== null) {
-        updateAdditional(
-          iframeReference.contentDocument,
-          additionalReference,
-          currReferenceIndex + 1,
-          errorReferences.length,
-          offset => {
-            switchError(offset);
-          }
-        );
-      } else {
-        if (errorReferences.length !== 1) {
-          throw new Error('Something is *really* wrong.');
-        }
-        renderErrorByIndex((currReferenceIndex = 0));
-      }
-    })
-    .catch(e => {
-      console.log('Could not consume error:', e);
-    });
-}
-
-function shortcutHandler(type: string) {
-  switch (type) {
-    case SHORTCUT_ESCAPE: {
-      unmount();
-      break;
-    }
-    case SHORTCUT_LEFT: {
-      switchError(-1);
-      break;
-    }
-    case SHORTCUT_RIGHT: {
-      switchError(1);
-      break;
-    }
-    default: {
-      //TODO: this
-      break;
-    }
-  }
-}
-
-function inject() {
-  registerError(window, error => crash(error));
-  registerPromise(window, error => crash(error, true));
-  registerShortcuts(window, shortcutHandler);
-  registerStackTraceLimit();
-
-  registerReactStack();
-  permanentRegisterConsole('error', (warning, stack) => {
-    const data = massageWarning(warning, stack);
-    crash(
-      // $FlowFixMe
-      {
-        message: data.message,
-        stack: data.stack,
-        __unmap_source: '/static/js/bundle.js',
-      },
-      false
-    );
-  });
-}
-
-function uninject() {
-  unregisterStackTraceLimit();
-  unregisterShortcuts(window);
-  unregisterPromise(window);
-  unregisterError(window);
-  unregisterReactStack();
-}
-
-export { inject, uninject };
diff --git a/packages/react-error-overlay/src/styles.js b/packages/react-error-overlay/src/styles.js
index bf17561d721..d6557c5d95f 100644
--- a/packages/react-error-overlay/src/styles.js
+++ b/packages/react-error-overlay/src/styles.js
@@ -24,7 +24,7 @@ const iframeStyle = {
   width: '100%',
   height: '100%',
   border: 'none',
-  'z-index': 2147483647 - 1, // below the compile error overlay
+  'z-index': 2147483647,
 };
 
 const overlayStyle = {
@@ -35,84 +35,6 @@ const overlayStyle = {
   'background-color': white,
 };
 
-const containerStyle = {
-  position: 'relative',
-  display: 'inline-flex',
-  'flex-direction': 'column',
-  height: '100%',
-  width: '1024px',
-  'max-width': '100%',
-  'overflow-x': 'hidden',
-  'overflow-y': 'auto',
-  padding: '0.5rem',
-  'box-sizing': 'border-box',
-  'text-align': 'left',
-  'font-family': 'Consolas, Menlo, monospace',
-  'font-size': '11px',
-  'white-space': 'pre-wrap',
-  'word-break': 'break-word',
-  'line-height': 1.5,
-  color: black,
-};
-
-const hintsStyle = {
-  color: darkGray,
-};
-
-const hintStyle = {
-  padding: '0.5em 1em',
-  cursor: 'pointer',
-};
-
-const closeButtonStyle = {
-  color: black,
-  'line-height': '1rem',
-  'font-size': '1.5rem',
-  padding: '1rem',
-  cursor: 'pointer',
-  position: 'absolute',
-  right: 0,
-  top: 0,
-};
-
-const additionalChildStyle = {
-  'margin-bottom': '0.5rem',
-};
-
-const headerStyle = {
-  'font-size': '2em',
-  'font-family': 'sans-serif',
-  color: red,
-  'white-space': 'pre-wrap',
-  // Top bottom margin spaces header
-  // Right margin revents overlap with close button
-  margin: '0 2rem 0.75rem 0',
-  flex: '0 0 auto',
-  'max-height': '50%',
-  overflow: 'auto',
-};
-
-const functionNameStyle = {};
-
-const linkStyle = {
-  'font-size': '0.9em',
-  'margin-bottom': '0.9em',
-};
-
-const anchorStyle = {
-  'text-decoration': 'none',
-  color: darkGray,
-};
-
-const traceStyle = {
-  'font-size': '1em',
-  flex: '0 1 auto',
-  'min-height': '0px',
-  overflow: 'auto',
-};
-
-const depStyle = {};
-
 const primaryErrorStyle = {
   'background-color': lightRed,
 };
@@ -121,104 +43,14 @@ const secondaryErrorStyle = {
   'background-color': yellow,
 };
 
-const omittedFramesCollapsedStyle = {
-  color: black,
-  cursor: 'pointer',
-  'margin-bottom': '1.5em',
-};
-
-const omittedFramesExpandedStyle = {
-  color: black,
-  cursor: 'pointer',
-  'margin-bottom': '0.6em',
-};
-
-const _preStyle = {
-  display: 'block',
-  padding: '0.5em',
-  'margin-top': '0.5em',
-  'margin-bottom': '0.5em',
-  'overflow-x': 'auto',
-  'white-space': 'pre-wrap',
-  'border-radius': '0.25rem',
-};
-const primaryPreStyle = Object.assign({}, _preStyle, {
-  'background-color': redTransparent,
-});
-const secondaryPreStyle = Object.assign({}, _preStyle, {
-  'background-color': yellowTransparent,
-});
-
-const toggleStyle = {
-  'margin-bottom': '1.5em',
-  color: darkGray,
-  cursor: 'pointer',
-};
-
-const codeStyle = {
-  'font-family': 'Consolas, Menlo, monospace',
-};
-
-const hiddenStyle = {
-  display: 'none',
-};
-
-const groupStyle = {
-  'margin-right': '1em',
-};
-
-const _groupElemStyle = {
-  'background-color': redTransparent,
-  color: red,
-  border: 'none',
-  'border-radius': '4px',
-  padding: '3px 6px',
-  cursor: 'pointer',
-};
-
-const groupElemLeft = Object.assign({}, _groupElemStyle, {
-  'border-top-right-radius': '0px',
-  'border-bottom-right-radius': '0px',
-  'margin-right': '1px',
-});
-
-const groupElemRight = Object.assign({}, _groupElemStyle, {
-  'border-top-left-radius': '0px',
-  'border-bottom-left-radius': '0px',
-});
-
-const footerStyle = {
-  'font-family': 'sans-serif',
-  color: darkGray,
-  'margin-top': '0.5rem',
-  flex: '0 0 auto',
-};
-
 export {
-  containerStyle,
   iframeStyle,
   overlayStyle,
-  hintsStyle,
-  hintStyle,
-  closeButtonStyle,
-  additionalChildStyle,
-  headerStyle,
-  functionNameStyle,
-  linkStyle,
-  anchorStyle,
-  traceStyle,
-  depStyle,
   primaryErrorStyle,
-  primaryPreStyle,
   secondaryErrorStyle,
-  secondaryPreStyle,
-  omittedFramesCollapsedStyle,
-  omittedFramesExpandedStyle,
-  toggleStyle,
-  codeStyle,
-  hiddenStyle,
-  groupStyle,
-  groupElemLeft,
-  groupElemRight,
-  footerStyle,
+  black,
+  darkGray,
+  red,
+  redTransparent,
+  yellowTransparent,
 };
diff --git a/packages/react-error-overlay/src/utils/dom/enableTabClick.js b/packages/react-error-overlay/src/utils/dom/enableTabClick.js
deleted file mode 100644
index b663c055f8c..00000000000
--- a/packages/react-error-overlay/src/utils/dom/enableTabClick.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
-
-/* @flow */
-function enableTabClick(node: Element) {
-  node.setAttribute('tabindex', '0');
-  node.addEventListener('keydown', function(e: KeyboardEvent) {
-    const { key, which, keyCode } = e;
-    if (key === 'Enter' || which === 13 || keyCode === 13) {
-      e.preventDefault();
-      if (typeof e.target.click === 'function') {
-        e.target.click();
-      }
-    }
-  });
-}
-
-export { enableTabClick };
diff --git a/packages/react-dev-utils/ansiHTML.js b/packages/react-error-overlay/src/utils/generateAnsiHTML.js
similarity index 95%
rename from packages/react-dev-utils/ansiHTML.js
rename to packages/react-error-overlay/src/utils/generateAnsiHTML.js
index 90bf70374ee..509daddc23c 100644
--- a/packages/react-dev-utils/ansiHTML.js
+++ b/packages/react-error-overlay/src/utils/generateAnsiHTML.js
@@ -7,10 +7,11 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-'use strict';
+/* @flow */
+
+import Anser from 'anser';
+import { AllHtmlEntities as Entities } from 'html-entities';
 
-var Anser = require('anser');
-var Entities = require('html-entities').AllHtmlEntities;
 var entities = new Entities();
 
 // Color scheme inspired by https://chriskempson.github.io/base16/css/base16-github.css
@@ -62,7 +63,7 @@ var anserMap = {
   'ansi-white': 'darkgrey',
 };
 
-function ansiHTML(txt) {
+function generateAnsiHTML(txt: string): string {
   var arr = new Anser().ansiToJson(entities.encode(txt), {
     use_classes: true,
   });
@@ -104,4 +105,4 @@ function ansiHTML(txt) {
   return result;
 }
 
-module.exports = ansiHTML;
+export default generateAnsiHTML;
diff --git a/packages/react-error-overlay/src/utils/getLinesAround.js b/packages/react-error-overlay/src/utils/getLinesAround.js
index a03e09a5817..7cb2ea5c54d 100644
--- a/packages/react-error-overlay/src/utils/getLinesAround.js
+++ b/packages/react-error-overlay/src/utils/getLinesAround.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-//@flow
+/* @flow */
 import { ScriptLine } from './stack-frame';
 
 /**
diff --git a/packages/react-error-overlay/src/utils/getPrettyURL.js b/packages/react-error-overlay/src/utils/getPrettyURL.js
new file mode 100644
index 00000000000..47b834d08d0
--- /dev/null
+++ b/packages/react-error-overlay/src/utils/getPrettyURL.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+function getPrettyURL(
+  sourceFileName: ?string,
+  sourceLineNumber: ?number,
+  sourceColumnNumber: ?number,
+  fileName: ?string,
+  lineNumber: ?number,
+  columnNumber: ?number,
+  compiled: boolean
+): string {
+  let prettyURL;
+  if (!compiled && sourceFileName && typeof sourceLineNumber === 'number') {
+    // Remove everything up to the first /src/ or /node_modules/
+    const trimMatch = /^[/|\\].*?[/|\\]((src|node_modules)[/|\\].*)/.exec(
+      sourceFileName
+    );
+    if (trimMatch && trimMatch[1]) {
+      prettyURL = trimMatch[1];
+    } else {
+      prettyURL = sourceFileName;
+    }
+    prettyURL += ':' + sourceLineNumber;
+    // Note: we intentionally skip 0's because they're produced by cheap Webpack maps
+    if (sourceColumnNumber) {
+      prettyURL += ':' + sourceColumnNumber;
+    }
+  } else if (fileName && typeof lineNumber === 'number') {
+    prettyURL = fileName + ':' + lineNumber;
+    // Note: we intentionally skip 0's because they're produced by cheap Webpack maps
+    if (columnNumber) {
+      prettyURL += ':' + columnNumber;
+    }
+  } else {
+    prettyURL = 'unknown';
+  }
+  return prettyURL.replace('webpack://', '.');
+}
+
+export { getPrettyURL };
+export default getPrettyURL;
diff --git a/packages/react-error-overlay/src/utils/getSourceMap.js b/packages/react-error-overlay/src/utils/getSourceMap.js
index 80dd6002e0e..1d8405519bd 100644
--- a/packages/react-error-overlay/src/utils/getSourceMap.js
+++ b/packages/react-error-overlay/src/utils/getSourceMap.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-//@flow
+/* @flow */
 import { SourceMapConsumer } from 'source-map';
 
 /**
diff --git a/packages/react-error-overlay/src/utils/errorRegister.js b/packages/react-error-overlay/src/utils/getStackFrames.js
similarity index 59%
rename from packages/react-error-overlay/src/utils/errorRegister.js
rename to packages/react-error-overlay/src/utils/getStackFrames.js
index 0bd3379cef1..e5a4073106b 100644
--- a/packages/react-error-overlay/src/utils/errorRegister.js
+++ b/packages/react-error-overlay/src/utils/getStackFrames.js
@@ -13,22 +13,11 @@ import { parse } from './parser';
 import { map } from './mapper';
 import { unmap } from './unmapper';
 
-type ErrorRecord = {
-  error: Error,
-  unhandledRejection: boolean,
-  contextSize: number,
-  enhancedFrames: StackFrame[],
-};
-type ErrorRecordReference = number;
-const recorded: ErrorRecord[] = [];
-
-let errorsConsumed: ErrorRecordReference = 0;
-
-function consume(
+function getStackFrames(
   error: Error,
   unhandledRejection: boolean = false,
   contextSize: number = 3
-): Promise<ErrorRecordReference | null> {
+): Promise<StackFrame[] | null> {
   const parsedFrames = parse(error);
   let enhancedFramesPromise;
   if (error.__unmap_source) {
@@ -49,32 +38,13 @@ function consume(
     ) {
       return null;
     }
-    enhancedFrames = enhancedFrames.filter(
+    return enhancedFrames.filter(
       ({ functionName }) =>
         functionName == null ||
         functionName.indexOf('__stack_frame_overlay_proxy_console__') === -1
     );
-    recorded[++errorsConsumed] = {
-      error,
-      unhandledRejection,
-      contextSize,
-      enhancedFrames,
-    };
-    return errorsConsumed;
   });
 }
 
-function getErrorRecord(ref: ErrorRecordReference): ErrorRecord {
-  return recorded[ref];
-}
-
-function drain() {
-  // $FlowFixMe
-  const keys = Object.keys(recorded);
-  for (let index = 0; index < keys.length; ++index) {
-    delete recorded[keys[index]];
-  }
-}
-
-export { consume, getErrorRecord, drain };
-export type { ErrorRecordReference };
+export default getStackFrames;
+export { getStackFrames };
diff --git a/packages/react-error-overlay/src/utils/isBultinErrorName.js b/packages/react-error-overlay/src/utils/isBultinErrorName.js
new file mode 100644
index 00000000000..cf732b838f4
--- /dev/null
+++ b/packages/react-error-overlay/src/utils/isBultinErrorName.js
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/* @flow */
+function isBultinErrorName(errorName: ?string) {
+  switch (errorName) {
+    case 'EvalError':
+    case 'InternalError':
+    case 'RangeError':
+    case 'ReferenceError':
+    case 'SyntaxError':
+    case 'TypeError':
+    case 'URIError':
+      return true;
+    default:
+      return false;
+  }
+}
+
+export { isBultinErrorName };
+export default isBultinErrorName;
diff --git a/packages/react-error-overlay/src/utils/mapper.js b/packages/react-error-overlay/src/utils/mapper.js
index fc3eb2266e6..656c216c144 100644
--- a/packages/react-error-overlay/src/utils/mapper.js
+++ b/packages/react-error-overlay/src/utils/mapper.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-// @flow
+/* @flow */
 import StackFrame from './stack-frame';
 import { getSourceMap } from './getSourceMap';
 import { getLinesAround } from './getLinesAround';
diff --git a/packages/react-error-overlay/src/utils/parser.js b/packages/react-error-overlay/src/utils/parser.js
index bfbb85f7969..1cbee1d1ccc 100644
--- a/packages/react-error-overlay/src/utils/parser.js
+++ b/packages/react-error-overlay/src/utils/parser.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-// @flow
+/* @flow */
 import StackFrame from './stack-frame';
 
 const regexExtractLocation = /\(?(.+?)(?::(\d+))?(?::(\d+))?\)?$/;
diff --git a/packages/react-error-overlay/src/utils/stack-frame.js b/packages/react-error-overlay/src/utils/stack-frame.js
index ae28cb53585..49f9da7fa11 100644
--- a/packages/react-error-overlay/src/utils/stack-frame.js
+++ b/packages/react-error-overlay/src/utils/stack-frame.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-//@flow
+/* @flow */
 
 /** A container holding a script line. */
 class ScriptLine {
@@ -54,6 +54,20 @@ class StackFrame {
     sourceColumnNumber: number | null = null,
     sourceScriptCode: ScriptLine[] | null = null
   ) {
+    if (functionName && functionName.indexOf('Object.') === 0) {
+      functionName = functionName.slice('Object.'.length);
+    }
+    if (
+      // Chrome has a bug with inferring function.name:
+      // https://github.com/facebookincubator/create-react-app/issues/2097
+      // Let's ignore a meaningless name we get for top-level modules.
+      functionName === 'friendlySyntaxErrorLabel' ||
+      functionName === 'exports.__esModule' ||
+      functionName === '<anonymous>' ||
+      !functionName
+    ) {
+      functionName = null;
+    }
     this.functionName = functionName;
 
     this.fileName = fileName;
@@ -72,8 +86,8 @@ class StackFrame {
   /**
    * Returns the name of this function.
    */
-  getFunctionName(): string | null {
-    return this.functionName;
+  getFunctionName(): string {
+    return this.functionName || '(anonymous function)';
   }
 
   /**
@@ -98,11 +112,9 @@ class StackFrame {
    * Returns a pretty version of this stack frame.
    */
   toString(): string {
-    const f = this.getFunctionName();
-    if (f == null) {
-      return this.getSource();
-    }
-    return `${f} (${this.getSource()})`;
+    const functionName = this.getFunctionName();
+    const source = this.getSource();
+    return `${functionName}${source ? ` (${source})` : ``}`;
   }
 }
 
diff --git a/packages/react-error-overlay/src/utils/unmapper.js b/packages/react-error-overlay/src/utils/unmapper.js
index b01736d74aa..60b2bee432b 100644
--- a/packages/react-error-overlay/src/utils/unmapper.js
+++ b/packages/react-error-overlay/src/utils/unmapper.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-// @flow
+/* @flow */
 import StackFrame from './stack-frame';
 import { getSourceMap } from './getSourceMap';
 import { getLinesAround } from './getLinesAround';
diff --git a/packages/react-error-overlay/src/utils/warnings.js b/packages/react-error-overlay/src/utils/warnings.js
index 8dcd7e1ee30..bcc54ceb733 100644
--- a/packages/react-error-overlay/src/utils/warnings.js
+++ b/packages/react-error-overlay/src/utils/warnings.js
@@ -7,7 +7,7 @@
  * of patent rights can be found in the PATENTS file in the same directory.
  */
 
-// @flow
+/* @flow */
 import type { ReactFrame } from '../effects/proxyConsole';
 
 function stripInlineStacktrace(message: string): string {
diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js
index 1261de6be19..a8f37e813ed 100755
--- a/packages/react-scripts/bin/react-scripts.js
+++ b/packages/react-scripts/bin/react-scripts.js
@@ -13,8 +13,9 @@
 const spawn = require('react-dev-utils/crossSpawn');
 const args = process.argv.slice(2);
 
-const scriptIndex = args.findIndex(x =>
-   x === 'build' || x === 'eject' || x === 'start' || x === 'test');
+const scriptIndex = args.findIndex(
+  x => x === 'build' || x === 'eject' || x === 'start' || x === 'test'
+);
 const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
 const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
 
diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js
index e9f985909da..3ef1104a177 100644
--- a/packages/react-scripts/config/webpack.config.dev.js
+++ b/packages/react-scripts/config/webpack.config.dev.js
@@ -56,8 +56,6 @@ module.exports = {
     require.resolve('react-dev-utils/webpackHotDevClient'),
     // We ship a few polyfills by default:
     require.resolve('./polyfills'),
-    // Errors should be considered fatal in development
-    require.resolve('react-error-overlay'),
     // Finally, this is your app's code:
     paths.appIndexJs,
     // We include the app code last so that if there is a runtime error during
diff --git a/packages/react-scripts/config/webpackDevServer.config.js b/packages/react-scripts/config/webpackDevServer.config.js
index 2a351e668bb..9c3889abae8 100644
--- a/packages/react-scripts/config/webpackDevServer.config.js
+++ b/packages/react-scripts/config/webpackDevServer.config.js
@@ -10,7 +10,7 @@
 // @remove-on-eject-end
 'use strict';
 
-const errorOverlayMiddleware = require('react-error-overlay/middleware');
+const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
 const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
 const config = require('./webpack.config.dev');
 const paths = require('./paths');
diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json
index bf6c6be48f2..fa74ffa5815 100644
--- a/packages/react-scripts/package.json
+++ b/packages/react-scripts/package.json
@@ -49,7 +49,6 @@
     "postcss-loader": "2.0.6",
     "promise": "8.0.1",
     "react-dev-utils": "^3.1.0",
-    "react-error-overlay": "^1.0.10",
     "style-loader": "0.18.2",
     "sw-precache-webpack-plugin": "0.11.4",
     "url-loader": "0.5.9",