Bug 1245921 - Turn toolbox toolbar into a React component r+miker draft
authorGreg Tatum <tatum.creative@gmail.com>
Fri, 18 Nov 2016 15:02:21 -0600
changeset 454825 51c4bc9d06b4cd84786a5b1542902dafb81393ac
parent 454824 ec73912c759c342d1f876d6646c17f8e6ff69949
child 540831 c81958ac8fb9a6dd4e39f32efc5337d7f6317513
push id40064
push userbmo:gtatum@mozilla.com
push dateFri, 30 Dec 2016 14:26:59 +0000
bugs1245921
milestone53.0a1
Bug 1245921 - Turn toolbox toolbar into a React component r+miker MozReview-Commit-ID: 4UZbcfw2YI9
addon-sdk/source/test/test-dev-panel.js
devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
devtools/client/definitions.js
devtools/client/framework/components/moz.build
devtools/client/framework/components/toolbox-controller.js
devtools/client/framework/components/toolbox-tab.js
devtools/client/framework/components/toolbox-toolbar.js
devtools/client/framework/moz.build
devtools/client/framework/test/browser_devtools_api.js
devtools/client/framework/test/browser_new_activation_workflow.js
devtools/client/framework/test/browser_toolbox_dynamic_registration.js
devtools/client/framework/test/browser_toolbox_highlight.js
devtools/client/framework/test/browser_toolbox_hosts.js
devtools/client/framework/test/browser_toolbox_options.js
devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js
devtools/client/framework/test/browser_toolbox_transport_events.js
devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
devtools/client/framework/toolbox-highlighter-utils.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox.js
devtools/client/framework/toolbox.xul
devtools/client/inspector/test/browser_inspector_highlighter-preview.js
devtools/client/inspector/test/browser_inspector_initialization.js
devtools/client/locales/en-US/toolbox.dtd
devtools/client/locales/en-US/toolbox.properties
devtools/client/performance/test/browser_perf-highlighted.js
devtools/client/preferences/devtools.js
devtools/client/shared/developer-toolbar.js
devtools/client/shared/test/browser_telemetry_button_paintflashing.js
devtools/client/shared/test/browser_telemetry_button_scratchpad.js
devtools/client/themes/firebug-theme.css
devtools/client/themes/toolbars.css
devtools/client/themes/toolbox.css
devtools/client/webconsole/test/browser_webconsole_split.js
devtools/client/webconsole/test/browser_webconsole_split_persist.js
--- a/addon-sdk/source/test/test-dev-panel.js
+++ b/addon-sdk/source/test/test-dev-panel.js
@@ -22,21 +22,23 @@ const iconURI = "data:image/png;base64,i
 const makeHTML = fn =>
   "data:text/html;charset=utf-8,<script>(" + fn + ")();</script>";
 
 
 const test = function(unit) {
   return function*(assert) {
     assert.isRendered = (panel, toolbox) => {
       const doc = toolbox.doc;
-      assert.ok(doc.querySelector("[value='" + panel.label + "']"),
-                "panel.label is found in the developer toolbox DOM");
-      assert.ok(doc.querySelector("[tooltiptext='" + panel.tooltip + "']"),
-                "panel.tooltip is found in the developer toolbox DOM");
-
+      assert.ok(Array.from(doc.querySelectorAll(".devtools-tab"))
+                     .find(el => el.textContent === panel.label),
+                "panel.label is found in the developer toolbox DOM " + panel.label);
+      if (panel.tooltip) {
+        assert.ok(doc.querySelector("[title='" + panel.tooltip + "']"),
+                  `panel.tooltip is found in the developer toolbox DOM "${panel.tooltip}"`);
+      }
       assert.ok(doc.querySelector("#toolbox-panel-" + panel.id),
                 "toolbar panel with a matching id is present");
     };
 
 
     yield* unit(assert);
   };
 };
--- a/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addon-panels.js
@@ -12,38 +12,38 @@ const ADDON_PATH = "addon3.xpi";
 var gAddon, gClient, gThreadClient, gDebugger, gSources;
 var PREFS = [
   ["devtools.canvasdebugger.enabled", true],
   ["devtools.shadereditor.enabled", true],
   ["devtools.performance.enabled", true],
   ["devtools.netmonitor.enabled", true],
   ["devtools.scratchpad.enabled", true]
 ];
+
 function test() {
   Task.spawn(function* () {
     // Store and enable all optional dev tools panels
     yield pushPrefs(...PREFS);
 
     let addon = yield addTemporaryAddon(ADDON_PATH);
     let addonDebugger = yield initAddonDebugger(ADDON_ID);
 
     // Check only valid tabs are shown
-    let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children;
+    let tabs = addonDebugger.frame.contentDocument.querySelectorAll(".toolbox-tabs button")
+
     let expectedTabs = ["webconsole", "jsdebugger", "scratchpad"];
 
     is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger");
     Array.forEach(tabs, (tab, i) => {
       let toolName = expectedTabs[i];
-      is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);
+      is(tab.getAttribute("data-id"), toolName, "displaying " + toolName);
     });
 
     // Check no toolbox buttons are shown
-    let buttons = addonDebugger.frame.contentDocument.getElementById("toolbox-buttons").children;
-    Array.forEach(buttons, (btn, i) => {
-      is(btn.hidden, true, "no toolbox buttons for the addon debugger -- " + btn.className);
-    });
+    let buttons = addonDebugger.frame.contentDocument.querySelectorAll("#toolbox-buttons-end button");
+    is(buttons.length, 0, "no toolbox buttons for the addon debugger");
 
     yield addonDebugger.destroy();
     yield removeAddon(addon);
 
     finish();
   });
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-highlight.js
@@ -30,28 +30,24 @@ function test() {
 }
 
 function testPause() {
   is(gDebugger.gThreadClient.paused, false,
     "Should be running after starting test.");
 
   gDebugger.gThreadClient.addOneTimeListener("paused", () => {
     gToolbox.selectTool("webconsole").then(() => {
-      ok(gToolboxTab.hasAttribute("highlighted") &&
-         gToolboxTab.getAttribute("highlighted") == "true",
+      ok(gToolboxTab.classList.contains("highlighted"),
         "The highlighted class is present");
-      ok(!gToolboxTab.hasAttribute("selected") ||
-          gToolboxTab.getAttribute("selected") != "true",
+      ok(!gToolboxTab.classList.contains("selected"),
         "The tab is not selected");
     }).then(() => gToolbox.selectTool("jsdebugger")).then(() => {
-      ok(gToolboxTab.hasAttribute("highlighted") &&
-         gToolboxTab.getAttribute("highlighted") == "true",
+      ok(gToolboxTab.classList.contains("highlighted"),
         "The highlighted class is present");
-      ok(gToolboxTab.hasAttribute("selected") &&
-         gToolboxTab.getAttribute("selected") == "true",
+      ok(gToolboxTab.classList.contains("selected"),
         "...and the tab is selected, so the glow will not be present.");
     }).then(testResume);
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gDebugger.document.getElementById("resume"),
     gDebugger);
 
@@ -61,18 +57,17 @@ function testPause() {
   });
 }
 
 function testResume() {
   gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
     gToolbox.selectTool("webconsole").then(() => {
       ok(!gToolboxTab.classList.contains("highlighted"),
         "The highlighted class is not present now after the resume");
-      ok(!gToolboxTab.hasAttribute("selected") ||
-          gToolboxTab.getAttribute("selected") != "true",
+      ok(!gToolboxTab.classList.contains("selected"),
         "The tab is not selected");
     }).then(() => closeDebuggerAndFinish(gPanel));
   });
 
   EventUtils.sendMouseEvent({ type: "mousedown" },
     gDebugger.document.getElementById("resume"),
     gDebugger);
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_on-pause-raise.js
@@ -74,46 +74,40 @@ add_task(function *() {
     yield onTabSelect;
 
     if (toolbox.hostType != Toolbox.HostType.WINDOW) {
       is(gBrowser.selectedTab, tab,
         "Debugger's tab got selected.");
     }
 
     yield toolbox.selectTool("webconsole");
-    ok(toolboxTab.hasAttribute("highlighted") &&
-       toolboxTab.getAttribute("highlighted") == "true",
+    ok(toolboxTab.classList.contains("highlighted"),
       "The highlighted class is present");
-    ok(!toolboxTab.hasAttribute("selected") ||
-        toolboxTab.getAttribute("selected") != "true",
+    ok(!toolboxTab.classList.contains("selected"),
       "The tab is not selected");
     yield toolbox.selectTool("jsdebugger");
-    ok(toolboxTab.hasAttribute("highlighted") &&
-       toolboxTab.getAttribute("highlighted") == "true",
+    ok(toolboxTab.classList.contains("highlighted"),
       "The highlighted class is present");
-    ok(toolboxTab.hasAttribute("selected") &&
-       toolboxTab.getAttribute("selected") == "true",
+    ok(toolboxTab.classList.contains("selected"),
       "...and the tab is selected, so the glow will not be present.");
   }
 
   function* testResume() {
     let onPaused = waitForEvent(panelWin.gThreadClient, "resumed");
 
     EventUtils.sendMouseEvent({ type: "mousedown" },
       panelWin.document.getElementById("resume"),
       panelWin);
 
     yield onPaused;
 
     yield toolbox.selectTool("webconsole");
-    ok(!toolboxTab.hasAttribute("highlighted") ||
-        toolboxTab.getAttribute("highlighted") != "true",
+    ok(!toolboxTab.classList.contains("highlighted"),
       "The highlighted class is not present now after the resume");
-    ok(!toolboxTab.hasAttribute("selected") ||
-        toolboxTab.getAttribute("selected") != "true",
+    ok(!toolboxTab.classList.contains("selected"),
       "The tab is not selected");
   }
 });
 
 registerCleanupFunction(function () {
   // Revert to the default toolbox host, so that the following tests proceed
   // normally and not inside a non-default host.
   Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
--- a/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-window.js
@@ -43,17 +43,17 @@ add_task(function* () {
         done();
       }
     });
   });
   ok(toolbox.win.parent.document.title.includes(WORKER_URL),
      "worker URL in host title");
 
   let toolTabs = toolbox.doc.querySelectorAll(".devtools-tab");
-  let activeTools = [...toolTabs].map(tab=>tab.getAttribute("toolid"));
+  let activeTools = [...toolTabs].map(tab=>tab.getAttribute("data-id"));
 
   is(activeTools.join(","), "webconsole,jsdebugger,scratchpad,options",
     "Correct set of tools supported by worker");
 
   terminateWorkerInTab(tab, WORKER_URL);
   yield waitForWorkerClose(workerClient);
   yield close(client);
 
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -465,16 +465,21 @@ exports.defaultThemes = [
   Tools.lightTheme,
   Tools.firebugTheme,
 ];
 
 // White-list buttons that can be toggled to prevent adding prefs for
 // addons that have manually inserted toolbarbuttons into DOM.
 // (By default, supported target is only local tab)
 exports.ToolboxButtons = [
+  { id: "command-button-pick",
+    isTargetSupported: target => {
+      return target.activeTab && target.activeTab.traits.frames;
+    }
+  },
   { id: "command-button-frames",
     isTargetSupported: target => {
       return target.activeTab && target.activeTab.traits.frames;
     }
   },
   { id: "command-button-splitconsole",
     isTargetSupported: target => !target.isAddon },
   { id: "command-button-responsive" },
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+DevToolsModules(
+  'toolbox-controller.js',
+  'toolbox-tab.js',
+  'toolbox-toolbar.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-controller.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {createClass, createFactory} = require("devtools/client/shared/vendor/react");
+const ToolboxToolbar = createFactory(require("devtools/client/framework/components/toolbox-toolbar"));
+const ELEMENT_PICKER_ID = "command-button-pick";
+
+/**
+ * This component serves as a state controller for the toolbox React component. It's a
+ * thin layer for translating events and state of the outside world into the React update
+ * cycle. This solution was used to keep the amount of code changes to a minimimum while
+ * adapting the existing codebase to start using React.
+ */
+module.exports = createClass({
+  displayName: "ToolboxController",
+
+  getInitialState() {
+    // See the ToolboxToolbar propTypes for documentation on each of these items in state,
+    // and for the defintions of the props that are expected to be passed in.
+    return {
+      focusedButton: ELEMENT_PICKER_ID,
+      currentToolId: null,
+      canRender: false,
+      highlightedTool: "",
+      areDockButtonsEnabled: true,
+      panelDefinitions: [],
+      hostTypes: [],
+      canCloseToolbox: true,
+      toolboxButtons: [],
+      buttonIds: [],
+      checkedButtonsUpdated: () => {
+        this.forceUpdate();
+      }
+    };
+  },
+
+  componentWillUnmount() {
+    this.state.toolboxButtons.forEach(button => {
+      button.off("updatechecked", this.state.checkedButtonsUpdated);
+    });
+  },
+
+  /**
+   * The button and tab ids must be known in order to be able to focus left and right
+   * using the arrow keys.
+   */
+  updateButtonIds() {
+    const {panelDefinitions, toolboxButtons, optionsPanel, hostTypes,
+           canCloseToolbox} = this.state;
+
+    // This is a little gnarly, but go through all of the state and extract the IDs.
+    this.setState({
+      buttonIds: [
+        ...toolboxButtons.filter(btn => btn.isInStartContainer).map(({id}) => id),
+        ...panelDefinitions.map(({id}) => id),
+        ...toolboxButtons.filter(btn => !btn.isInStartContainer).map(({id}) => id),
+        optionsPanel ? optionsPanel.id : null,
+        ...hostTypes.map(({position}) => "toolbox-dock-" + position),
+        canCloseToolbox ? "toolbox-close" : null
+      ].filter(id => id)
+    });
+
+    this.updateFocusedButton();
+  },
+
+  updateFocusedButton() {
+    this.setFocusedButton(this.state.focusedButton);
+  },
+
+  setFocusedButton(focusedButton) {
+    const {buttonIds} = this.state;
+
+    this.setState({
+      focusedButton: focusedButton && buttonIds.includes(focusedButton)
+        ? focusedButton
+        : buttonIds[0]
+    });
+  },
+
+  setCurrentToolId(currentToolId) {
+    this.setState({currentToolId});
+    // Also set the currently focused button to this tool.
+    this.setFocusedButton(currentToolId);
+  },
+
+  setCanRender() {
+    this.setState({ canRender: true });
+    this.updateButtonIds();
+  },
+
+  setOptionsPanel(optionsPanel) {
+    this.setState({ optionsPanel });
+    this.updateButtonIds();
+  },
+
+  highlightTool(highlightedTool) {
+    this.setState({ highlightedTool });
+  },
+
+  unhighlightTool(id) {
+    if (this.state.highlightedTool === id) {
+      this.setState({ highlightedTool: "" });
+    }
+  },
+
+  setDockButtonsEnabled(areDockButtonsEnabled) {
+    this.setState({ areDockButtonsEnabled });
+    this.updateButtonIds();
+  },
+
+  setHostTypes(hostTypes) {
+    this.setState({ hostTypes });
+    this.updateButtonIds();
+  },
+
+  setCanCloseToolbox(canCloseToolbox) {
+    this.setState({ canCloseToolbox });
+    this.updateButtonIds();
+  },
+
+  setPanelDefinitions(panelDefinitions) {
+    this.setState({ panelDefinitions });
+    this.updateButtonIds();
+  },
+
+  setToolboxButtons(toolboxButtons) {
+    // Listen for updates of the checked attribute.
+    this.state.toolboxButtons.forEach(button => {
+      button.off("updatechecked", this.state.checkedButtonsUpdated);
+    });
+    toolboxButtons.forEach(button => {
+      button.on("updatechecked", this.state.checkedButtonsUpdated);
+    });
+
+    this.setState({ toolboxButtons });
+    this.updateButtonIds();
+  },
+
+  setCanMinimize(canMinimize) {
+    /* Bug 1177463 - The minimize button is currently hidden until we agree on
+       the UI for it, and until bug 1173849 is fixed too. */
+
+    // this.setState({ canMinimize });
+  },
+
+  render() {
+    return ToolboxToolbar(Object.assign({}, this.props, this.state));
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-tab.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {img, button} = DOM;
+
+module.exports = createClass({
+  displayName: "ToolboxTab",
+
+  renderIcon(definition, isHighlighted) {
+    const {icon, highlightedicon} = definition;
+    if (!icon) {
+      return [];
+    }
+    return [
+      img({
+        className: "default-icon",
+        src: icon
+      }),
+      img({
+        className: "highlighted-icon",
+        src: highlightedicon || icon
+      })
+    ];
+  },
+
+  render() {
+    const {panelDefinition, currentToolId, highlightedTool, selectTool,
+           focusedButton, focusButton} = this.props;
+    const {id, tooltip, label, iconOnly} = panelDefinition;
+    const isHighlighted = id === currentToolId;
+
+    const className = [
+      "devtools-tab",
+      panelDefinition.invertIconForLightTheme || panelDefinition.invertIconForDarkTheme
+        ? "icon-invertable"
+        : "",
+      panelDefinition.invertIconForLightTheme ? "icon-invertable-light-theme" : "",
+      panelDefinition.invertIconForDarkTheme ? "icon-invertable-dark-theme" : "",
+      currentToolId === id ? "selected" : "",
+      highlightedTool === id ? "highlighted" : "",
+      iconOnly ? "devtools-tab-icon-only" : ""
+    ].join(" ");
+
+    return button(
+      {
+        className,
+        id: `toolbox-tab-${id}`,
+        "data-id": id,
+        title: tooltip,
+        type: "button",
+        tabIndex: focusedButton === id ? "0" : "-1",
+        onFocus: () => focusButton(id),
+        onClick: () => selectTool(id),
+      },
+      ...this.renderIcon(panelDefinition, isHighlighted),
+      iconOnly ? null : label
+    );
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-toolbar.js
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {DOM, createClass, createFactory, PropTypes} = require("devtools/client/shared/vendor/react");
+const {div, button} = DOM;
+const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
+
+/**
+ * This is the overall component for the toolbox toolbar. It is designed to not know how
+ * the state is being managed, and attempts to be as pure as possible. The
+ * ToolboxController component controls the changing state, and passes in everything as
+ * props.
+ */
+module.exports = createClass({
+  displayName: "ToolboxToolbar",
+
+  propTypes: {
+    // The currently focused item (for arrow keyboard navigation)
+    // This ID determines the tabindex being 0 or -1.
+    focusedButton: PropTypes.string,
+    // List of command button definitions.
+    toolboxButtons: PropTypes.array,
+    // The id of the currently selected tool, e.g. "inspector"
+    currentToolId: PropTypes.string,
+    // An optionally highlighted tool, e.g. "inspector"
+    highlightedTool: PropTypes.string,
+    // List of tool panel definitions.
+    panelDefinitions: PropTypes.array,
+    // Function to select a tool based on its id.
+    selectTool: PropTypes.func,
+    // Keep a record of what button is focused.
+    focusButton: PropTypes.func,
+    // The options button definition.
+    optionsPanel: PropTypes.object,
+    // Hold off displaying the toolbar until enough information is ready for it to render
+    // nicely.
+    canRender: PropTypes.bool,
+    // Localization interface.
+    L10N: PropTypes.object,
+  },
+
+  /**
+   * The render function is kept fairly short for maintainability. See the individual
+   * render functions for how each of the sections is rendered.
+   */
+  render() {
+    const containerProps = {className: "devtools-tabbar"};
+    return this.props.canRender
+      ? (
+        div(
+          containerProps,
+          renderToolboxButtonsStart(this.props),
+          renderTabs(this.props),
+          renderToolboxButtonsEnd(this.props),
+          renderOptions(this.props),
+          renderSeparator(),
+          renderDockButtons(this.props)
+        )
+      )
+      : div(containerProps);
+  }
+});
+
+/**
+ * Render all of the tabs, this takes in the panel definitions and builds out
+ * the buttons for each of them.
+ *
+ * @param {Array}    panelDefinitions - Array of objects that define panels.
+ * @param {String}   currentToolId - The currently selected tool's id; e.g. "inspector".
+ * @param {String}   highlightedTool - If a tool is highlighted, this is it's id.
+ * @param {Function} selectTool - Function to select a tool in the toolbox.
+ * @param {String}   focusedButton - The id of the focused button.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ */
+function renderTabs({panelDefinitions, currentToolId, highlightedTool, selectTool,
+                     focusedButton, focusButton}) {
+  // A wrapper is needed to get flex sizing correct in XUL.
+  return div({className: "toolbox-tabs-wrapper"},
+    div({className: "toolbox-tabs"},
+      ...panelDefinitions.map(panelDefinition => ToolboxTab({
+        panelDefinition,
+        currentToolId,
+        highlightedTool,
+        selectTool,
+        focusedButton,
+        focusButton,
+      }))
+    )
+  );
+}
+
+/**
+ * A little helper function to call renderToolboxButtons for buttons at the start
+ * of the toolbox.
+ */
+function renderToolboxButtonsStart(props) {
+  return renderToolboxButtons(props, true);
+}
+
+/**
+* A little helper function to call renderToolboxButtons for buttons at the end
+* of the toolbox.
+ */
+function renderToolboxButtonsEnd(props) {
+  return renderToolboxButtons(props, false);
+}
+
+/**
+ * Render all of the tabs, this takes in a list of toolbox button states. These are plain
+ * objects that have all of the relevant information needed to render the button.
+ * See Toolbox.prototype._createButtonState in devtools/client/framework/toolbox.js for
+ * documentation on this object.
+ *
+ * @param {Array} toolboxButtons - Array of objects that define the command buttons.
+ * @param {String} focusedButton - The id of the focused button.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ * @param {boolean} isStart - Render either the starting buttons, or ending buttons.
+ */
+function renderToolboxButtons({toolboxButtons, focusedButton, focusButton}, isStart) {
+  const visibleButtons = toolboxButtons.filter(command => {
+    const {isVisible, isInStartContainer} = command;
+    return isVisible && (isStart ? isInStartContainer : !isInStartContainer);
+  });
+
+  if (visibleButtons.length === 0) {
+    return null;
+  }
+
+  return div({id: `toolbox-buttons-${isStart ? "start" : "end"}`},
+    ...visibleButtons.map(command => {
+      const {id, description, onClick, isChecked, className: buttonClass} = command;
+      return button({
+        id,
+        title: description,
+        className: (
+          "command-button command-button-invertable devtools-button "
+          + buttonClass + (isChecked ? " checked" : "")
+        ),
+        onClick: (event) => {
+          onClick(event);
+          focusButton(id);
+        },
+        onFocus: () => focusButton(id),
+        tabIndex: id === focusedButton ? "0" : "-1"
+      });
+    })
+  );
+}
+
+/**
+ * The options button is a ToolboxTab just like in the renderTabs() function. However
+ * it is separate from the normal tabs, so deal with it separately here.
+ *
+ * @param {Object}   optionsPanel - A single panel definition for the options panel.
+ * @param {String}   currentToolId - The currently selected tool's id; e.g. "inspector".
+ * @param {Function} selectTool - Function to select a tool in the toolbox.
+ * @param {String}   focusedButton - The id of the focused button.
+ * @param {Function} focusButton - Keep a record of the currently focused button.
+ */
+function renderOptions({optionsPanel, currentToolId, selectTool, focusedButton,
+                        focusButton}) {
+  return div({id: "toolbox-option-container"}, ToolboxTab({
+    panelDefinition: optionsPanel,
+    currentToolId,
+    selectTool,
+    focusedButton,
+    focusButton,
+  }));
+}
+
+/**
+ * Render a separator.
+ */
+function renderSeparator() {
+  return div({
+    id: "toolbox-controls-separator",
+    className: "devtools-separator"
+  });
+}
+
+/**
+ * Render the dock buttons, and handle all the cases for what type of host the toolbox
+ * is attached to. The following props are expected.
+ *
+ * @property {String} focusedButton - The id of the focused button.
+ * @property {Function} closeToolbox - Completely close the toolbox.
+ * @property {Array} hostTypes - Array of host type objects, containing:
+ *                   @property {String} position - Position name
+ *                   @property {Function} switchHost - Function to switch the host.
+ * @property {Function} focusButton - Keep a record of the currently focused button.
+ * @property {Object} L10N - Localization interface.
+ * @property {Boolean} areDockButtonsEnabled - They are not enabled in certain situations
+ *                                             like when they are in the WebIDE.
+ * @property {Boolean} canCloseToolbox - Are the tools in a context where they can be
+ *                                       closed? This is not always the case, e.g. in the
+ *                                       WebIDE.
+ */
+function renderDockButtons(props) {
+  const {
+    focusedButton,
+    closeToolbox,
+    hostTypes,
+    focusButton,
+    L10N,
+    areDockButtonsEnabled,
+    canCloseToolbox,
+  } = props;
+
+  let buttons = [];
+
+  if (areDockButtonsEnabled) {
+    hostTypes.forEach(hostType => {
+      const id = "toolbox-dock-" + hostType.position;
+      buttons.push(button({
+        id,
+        onFocus: () => focusButton(id),
+        className: "toolbox-dock-button devtools-button",
+        title: L10N.getStr(`toolboxDockButtons.${hostType.position}.tooltip`),
+        onClick: e => {
+          hostType.switchHost();
+          focusButton(id);
+        },
+        tabIndex: focusedButton === id ? "0" : "-1",
+      }));
+    });
+  }
+
+  const closeButtonId = "toolbox-close";
+
+  const closeButton = canCloseToolbox
+    ? button({
+      id: closeButtonId,
+      onFocus: () => focusButton(closeButtonId),
+      className: "devtools-button",
+      title: L10N.getStr("toolbox.closebutton.tooltip"),
+      onClick: () => {
+        closeToolbox();
+        focusButton(closeButtonId);
+      },
+      tabIndex: focusedButton === "toolbox-close" ? "0" : "-1",
+    })
+    : null;
+
+  return div({id: "toolbox-controls"},
+    div({id: "toolbox-dock-buttons"}, ...buttons),
+    closeButton
+  );
+}
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -4,16 +4,20 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
     'test/shared-redux-head.js',
 ]
 
+DIRS += [
+    'components',
+]
+
 DevToolsModules(
     'about-devtools-toolbox.js',
     'attach-thread.js',
     'browser-menus.js',
     'devtools-browser.js',
     'devtools.js',
     'gDevTools.jsm',
     'location-store.js',
--- a/devtools/client/framework/test/browser_devtools_api.js
+++ b/devtools/client/framework/test/browser_devtools_api.js
@@ -147,21 +147,23 @@ function runTests2() {
     continueTests(toolbox);
   });
 }
 
 var continueTests = Task.async(function* (toolbox, panel) {
   ok(toolbox.getCurrentPanel(), "panel value is correct");
   is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
 
-  ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2).hasAttribute("icon-invertable"),
-    "The tool tab does not have the invertable attribute");
+  ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2)
+     .classList.contains("icon-invertable"),
+     "The tool tab does not have the invertable class");
 
-  ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"),
-    "The builtin tool tabs do have the invertable attribute");
+  ok(toolbox.doc.getElementById("toolbox-tab-inspector")
+     .classList.contains("icon-invertable"),
+     "The builtin tool tabs do have the invertable class");
 
   let toolDefinitions = gDevTools.getToolDefinitionMap();
   ok(toolDefinitions.has(toolId2), "The tool is in gDevTools");
 
   let toolDefinition = toolDefinitions.get(toolId2);
   is(toolDefinition.id, toolId2, "toolDefinition id is correct");
 
   info("Testing toolbox tool-unregistered event");
--- a/devtools/client/framework/test/browser_new_activation_workflow.js
+++ b/devtools/client/framework/test/browser_new_activation_workflow.js
@@ -36,17 +36,17 @@ function checkToolLoading() {
       testToggle();
     });
   });
 }
 
 function selectAndCheckById(id) {
   return toolbox.selectTool(id).then(function () {
     let tab = toolbox.doc.getElementById("toolbox-tab-" + id);
-    is(tab.hasAttribute("selected"), true, "The " + id + " tab is selected");
+    is(tab.classList.contains("selected"), true, "The " + id + " tab is selected");
   });
 }
 
 function testToggle() {
   toolbox.once("destroyed", () => {
     // Cannot reuse a target after it's destroyed.
     target = TargetFactory.forTab(gBrowser.selectedTab);
     gDevTools.showToolbox(target, "styleeditor").then(function (aToolbox) {
--- a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
+++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
@@ -92,13 +92,14 @@ function toolUnregistered(event, toolId)
     ok(!menuitem, "menu item removed from every browser window");
   }
 
   cleanup();
 }
 
 function cleanup()
 {
-  toolbox.destroy();
-  toolbox = null;
-  gBrowser.removeCurrentTab();
-  finish();
+  toolbox.destroy().then(() => {;
+    toolbox = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  })
 }
--- a/devtools/client/framework/test/browser_toolbox_highlight.js
+++ b/devtools/client/framework/test/browser_toolbox_highlight.js
@@ -3,79 +3,83 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var {Toolbox} = require("devtools/client/framework/toolbox");
 
 var toolbox = null;
 
 function test() {
-  const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
+  Task.spawn(function* () {
+    const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
 
-  const TOOL_ID_1 = "jsdebugger";
-  const TOOL_ID_2 = "webconsole";
+    const TOOL_ID_1 = "jsdebugger";
+    const TOOL_ID_2 = "webconsole";
+    yield addTab(URL);
+
+    const target = TargetFactory.forTab(gBrowser.selectedTab);
+    toolbox = yield gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
 
-  addTab(URL).then(() => {
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
-             .then(aToolbox => {
-               toolbox = aToolbox;
-                // select tool 2
-               toolbox.selectTool(TOOL_ID_2)
-                       // and highlight the first one
-                       .then(highlightTab.bind(null, TOOL_ID_1))
-                       // to see if it has the proper class.
-                       .then(checkHighlighted.bind(null, TOOL_ID_1))
-                       // Now switch back to first tool
-                       .then(() => toolbox.selectTool(TOOL_ID_1))
-                       // to check again. But there is no easy way to test if
-                       // it is showing orange or not.
-                       .then(checkNoHighlightWhenSelected.bind(null, TOOL_ID_1))
-                       // Switch to tool 2 again
-                       .then(() => toolbox.selectTool(TOOL_ID_2))
-                       // and check again.
-                       .then(checkHighlighted.bind(null, TOOL_ID_1))
-                       // Now unhighlight the tool
-                       .then(unhighlightTab.bind(null, TOOL_ID_1))
-                       // to see the classes gone.
-                       .then(checkNoHighlight.bind(null, TOOL_ID_1))
-                       // Now close the toolbox and exit.
-                       .then(() => executeSoon(() => {
-                         toolbox.destroy()
-                                 .then(() => {
-                                   toolbox = null;
-                                   gBrowser.removeCurrentTab();
-                                   finish();
-                                 });
-                       }));
-             });
+    // select tool 2
+    yield toolbox.selectTool(TOOL_ID_2)
+    // and highlight the first one
+    yield highlightTab(TOOL_ID_1);
+    // to see if it has the proper class.
+    yield checkHighlighted(TOOL_ID_1);
+    // Now switch back to first tool
+    yield toolbox.selectTool(TOOL_ID_1);
+    // to check again. But there is no easy way to test if
+    // it is showing orange or not.
+    yield checkNoHighlightWhenSelected(TOOL_ID_1);
+    // Switch to tool 2 again
+    yield toolbox.selectTool(TOOL_ID_2);
+    // and check again.
+    yield checkHighlighted(TOOL_ID_1);
+    // Now unhighlight the tool
+    yield unhighlightTab(TOOL_ID_1);
+    // to see the classes gone.
+    yield checkNoHighlight(TOOL_ID_1);
+
+    // Now close the toolbox and exit.
+    executeSoon(() => {
+      toolbox.destroy().then(() => {
+        toolbox = null;
+        gBrowser.removeCurrentTab();
+        finish();
+      });
+    });
+  })
+  .catch(error => {
+    ok(false, "There was an error running the test.");
   });
 }
 
 function highlightTab(toolId) {
-  info("Highlighting tool " + toolId + "'s tab.");
-  toolbox.highlightTool(toolId);
+  info(`Highlighting tool ${toolId}'s tab.`);
+  return toolbox.highlightTool(toolId);
 }
 
 function unhighlightTab(toolId) {
-  info("Unhighlighting tool " + toolId + "'s tab.");
-  toolbox.unhighlightTool(toolId);
+  info(`Unhighlighting tool ${toolId}'s tab.`);
+  return toolbox.unhighlightTool(toolId);
 }
 
 function checkHighlighted(toolId) {
   let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
-  ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
-  ok(!tab.hasAttribute("selected") || tab.getAttribute("selected") != "true",
-     "The tab is not selected");
+  ok(tab.classList.contains("highlighted"),
+     `The highlighted class is present in ${toolId}.`);
+  ok(!tab.classList.contains("selected"),
+     `The tab is not selected in ${toolId}`);
 }
 
 function checkNoHighlightWhenSelected(toolId) {
   let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
-  ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
-  ok(tab.hasAttribute("selected") && tab.getAttribute("selected") == "true",
-     "and the tab is selected, so the orange glow will not be present.");
+  ok(tab.classList.contains("highlighted"),
+     `The highlighted class is present in ${toolId}`);
+  ok(tab.classList.contains("selected"),
+     `And the tab is selected, so the orange glow will not be present. in ${toolId}`);
 }
 
 function checkNoHighlight(toolId) {
   let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
-  ok(!tab.hasAttribute("highlighted"),
-     "The highlighted attribute is not present");
+  ok(!tab.classList.contains("highlighted"),
+     `The highlighted class is not present in ${toolId}`);
 }
--- a/devtools/client/framework/test/browser_toolbox_hosts.js
+++ b/devtools/client/framework/test/browser_toolbox_hosts.js
@@ -129,11 +129,11 @@ function* testPreviousHost() {
   info("Forcing the previous host to match the current (side)");
   Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
   info("Switching from side to bottom (since previous=current=side");
   yield toolbox.switchToPreviousHost();
   checkHostType(toolbox, BOTTOM, SIDE);
 }
 
 function checkToolboxLoaded(iframe) {
-  let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
+  let tabs = iframe.contentDocument.querySelector(".toolbox-tabs");
   ok(tabs, "toolbox UI has been loaded into iframe");
 }
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -288,32 +288,18 @@ function checkUnregistered(toolId, defer
   }
   deferred.resolve();
 }
 
 function checkRegistered(toolId, deferred, event, data) {
   if (data == toolId) {
     ok(true, "Correct tool added back");
     // checking tab on the toolbox
-    let radio = doc.getElementById("toolbox-tab-" + toolId);
-    ok(radio, "Tab added back for " + toolId);
-    if (radio.previousSibling) {
-      ok(+radio.getAttribute("ordinal") >=
-         +radio.previousSibling.getAttribute("ordinal"),
-         "Inserted tab's ordinal is greater than equal to its previous tab." +
-         "Expected " + radio.getAttribute("ordinal") + " >= " +
-         radio.previousSibling.getAttribute("ordinal"));
-    }
-    if (radio.nextSibling) {
-      ok(+radio.getAttribute("ordinal") <
-         +radio.nextSibling.getAttribute("ordinal"),
-         "Inserted tab's ordinal is less than its next tab. Expected " +
-         radio.getAttribute("ordinal") + " < " +
-         radio.nextSibling.getAttribute("ordinal"));
-    }
+    let button = doc.getElementById("toolbox-tab-" + toolId);
+    ok(button, "Tab added back for " + toolId);
   } else {
     ok(false, "Something went wrong, " + toolId + " was not registered");
   }
   deferred.resolve();
 }
 
 function GetPref(name) {
   let type = Services.prefs.getPrefType(name);
--- a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -55,82 +55,83 @@ function testSelectTool(devtoolsToolbox)
 
   return deferred.promise;
 }
 
 function testPreferenceAndUIStateIsConsistent() {
   let checkNodes = [...panelWin.document.querySelectorAll(
     "#enabled-toolbox-buttons-box input[type=checkbox]")];
   let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
-  toolboxButtonNodes.push(doc.getElementById("command-button-frames"));
-  let toggleableTools = toolbox.toolboxButtons;
 
   // The noautohide button is only displayed in the browser toolbox
-  toggleableTools = toggleableTools.filter(
+  let toolbarButtons = toolbox.toolbarButtons.filter(
     tool => tool.id != "command-button-noautohide");
 
-  for (let tool of toggleableTools) {
+  for (let tool of toolbarButtons) {
     let isVisible = getBoolPref(tool.visibilityswitch);
 
-    let button = toolboxButtonNodes.filter(
-      toolboxButton => toolboxButton.id === tool.id)[0];
-    is(!button.hasAttribute("hidden"), isVisible,
+    let button = toolboxButtonNodes.find(toolboxButton => toolboxButton.id === tool.id);
+    is(!!button, isVisible,
       "Button visibility matches pref for " + tool.id);
 
     let check = checkNodes.filter(node => node.id === tool.id)[0];
     is(check.checked, isVisible,
       "Checkbox should be selected based on current pref for " + tool.id);
   }
 }
 
 function testToggleToolboxButtons() {
   let checkNodes = [...panelWin.document.querySelectorAll(
     "#enabled-toolbox-buttons-box input[type=checkbox]")];
-  let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
-  let toggleableTools = toolbox.toolboxButtons;
 
   // The noautohide button is only displayed in the browser toolbox, and the element
   // picker button is not toggleable.
-  toggleableTools = toggleableTools.filter(
-    tool => tool.id != "command-button-noautohide" && tool.id != "command-button-pick");
-  toolboxButtonNodes = toolboxButtonNodes.filter(
-    btn => btn.id != "command-button-noautohide" && btn.id != "command-button-pick");
+  let toolbarButtons = toolbox.toolbarButtons.filter(
+    tool => tool.id != "command-button-noautohide");
+
+  let visibleToolbarButtons = toolbox.toolbarButtons.filter(tool => tool.isVisible);
 
-  is(checkNodes.length, toggleableTools.length,
+  let toolbarButtonNodes = [...doc.querySelectorAll(".command-button")].filter(
+    btn => btn.id != "command-button-noautohide");
+
+  is(checkNodes.length, toolbarButtons.length,
     "All of the buttons are toggleable.");
-  is(checkNodes.length, toolboxButtonNodes.length,
+  is(visibleToolbarButtons.length, toolbarButtonNodes.length,
     "All of the DOM buttons are toggleable.");
 
-  for (let tool of toggleableTools) {
+  for (let tool of toolbarButtons) {
     let id = tool.id;
     let matchedCheckboxes = checkNodes.filter(node => node.id === id);
-    let matchedButtons = toolboxButtonNodes.filter(button => button.id === id);
+    let matchedButtons = toolbarButtonNodes.filter(button => button.id === id);
     is(matchedCheckboxes.length, 1,
       "There should be a single toggle checkbox for: " + id);
-    is(matchedButtons.length, 1,
-      "There should be a DOM button for: " + id);
-    is(matchedButtons[0], tool.button,
-      "DOM buttons should match for: " + id);
+    if (tool.isVisible) {
+      is(matchedButtons.length, 1,
+        "There should be a DOM button for the visible: " + id);
+      is(matchedButtons[0].getAttribute("title"), tool.description,
+        "The tooltip for button matches the tool definition.");
+    } else {
+      is(matchedButtons.length, 0,
+        "There should not be a DOM button for the invisible: " + id);
+    }
 
-    is(matchedCheckboxes[0].nextSibling.textContent, tool.label,
+    is(matchedCheckboxes[0].nextSibling.textContent, tool.description,
       "The label for checkbox matches the tool definition.");
-    is(matchedButtons[0].getAttribute("title"), tool.label,
-      "The tooltip for button matches the tool definition.");
   }
 
   // Store modified pref names so that they can be cleared on error.
-  for (let tool of toggleableTools) {
+  for (let tool of toolbarButtons) {
     let pref = tool.visibilityswitch;
     modifiedPrefs.push(pref);
   }
 
   // Try checking each checkbox, making sure that it changes the preference
   for (let node of checkNodes) {
-    let tool = toggleableTools.filter(
-      toggleableTool => toggleableTool.id === node.id)[0];
+    let tool = toolbarButtons.filter(
+      commandButton => commandButton.id === node.id)[0];
     let isVisible = getBoolPref(tool.visibilityswitch);
 
     testPreferenceAndUIStateIsConsistent();
     node.click();
     testPreferenceAndUIStateIsConsistent();
 
     let isVisibleAfterClick = getBoolPref(tool.visibilityswitch);
 
--- a/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js
+++ b/devtools/client/framework/test/browser_toolbox_tools_per_toolbox_registration.js
@@ -127,13 +127,14 @@ function toolboxToolUnregistered() {
 
   let panel = doc.getElementById("toolbox-panel-" + TOOL_ID);
   ok(!panel, "tool's panel was removed from toolbox UI");
 
   cleanup();
 }
 
 function cleanup() {
-  toolbox.destroy();
-  toolbox = null;
-  gBrowser.removeCurrentTab();
-  finish();
+  toolbox.destroy().then(() => {;
+    toolbox = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  });
 }
--- a/devtools/client/framework/test/browser_toolbox_transport_events.js
+++ b/devtools/client/framework/test/browser_toolbox_transport_events.js
@@ -25,20 +25,22 @@ function testResults(toolbox) {
   cleanUp(toolbox);
 }
 
 function cleanUp(toolbox) {
   gDevTools.off("toolbox-created", onToolboxCreated);
   off(DebuggerClient, "connect", onDebuggerClientConnect);
 
   toolbox.destroy().then(function () {
-    gBrowser.removeCurrentTab();
-    executeSoon(function () {
-      finish();
-    });
+    setTimeout(() => {
+      gBrowser.removeCurrentTab();
+      executeSoon(function () {
+        finish();
+      });
+    }, 1000);
   });
 }
 
 function testPackets(sent, received) {
   ok(sent.length > 0, "There must be at least one sent packet");
   ok(received.length > 0, "There must be at leaset one received packet");
 
   if (!sent.length || received.length) {
--- a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -39,21 +39,21 @@ add_task(function* () {
 
   // Wait for tick to avoid unexpected 'popuphidden' event, which
   // blocks the frame popup menu opened below. See also bug 1276873
   yield waitForTick();
 
   // Open frame menu and wait till it's available on the screen.
   // Also check 'open' attribute on the command button.
   let btn = toolbox.doc.getElementById("command-button-frames");
-  ok(!btn.getAttribute("open"), "The open attribute must not be present");
+  ok(!btn.classList.contains("checked"), "The checked class must not be present");
   let menu = toolbox.showFramesMenu({target: btn});
   yield once(menu, "open");
 
-  is(btn.getAttribute("open"), "true", "The open attribute must be set");
+  ok(btn.classList.contains("checked"), "The checked class must be set");
 
   // Verify that the frame list menu is populated
   let frames = menu.items;
   is(frames.length, 2, "We have both frames in the list");
 
   let topFrameBtn = frames.filter(b => b.label == URL)[0];
   let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
   ok(topFrameBtn, "Got top level document in the list");
--- a/devtools/client/framework/toolbox-highlighter-utils.js
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -117,17 +117,17 @@ exports.getHighlighterUtils = function (
    * if it is already started
    */
   let startPicker = exported.startPicker = requireInspector(function* (doFocus = false) {
     if (isPicking) {
       return;
     }
     isPicking = true;
 
-    toolbox.pickerButtonChecked = true;
+    toolbox.pickerButton.isChecked = true;
     yield toolbox.selectTool("inspector");
     toolbox.on("select", cancelPicker);
 
     if (isRemoteHighlightable()) {
       toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
       toolbox.walker.on("picker-node-picked", onPickerNodePicked);
       toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
       toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
@@ -151,17 +151,17 @@ exports.getHighlighterUtils = function (
    * if it is already stopped
    */
   let stopPicker = exported.stopPicker = requireInspector(function* () {
     if (!isPicking) {
       return;
     }
     isPicking = false;
 
-    toolbox.pickerButtonChecked = false;
+    toolbox.pickerButton.isChecked = false;
 
     if (isRemoteHighlightable()) {
       yield toolbox.highlighter.cancelPick();
       toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
       toolbox.walker.off("picker-node-picked", onPickerNodePicked);
       toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
       toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
     } else {
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -130,58 +130,64 @@ OptionsPanel.prototype = {
     let themeBox = this.panelDoc.getElementById("devtools-theme-box");
     let themeInput = themeBox.querySelector(`[value=${theme.id}]`);
 
     if (themeInput) {
       themeInput.parentNode.remove();
     }
   },
 
-  setupToolbarButtonsList: function () {
+  setupToolbarButtonsList: Task.async(function* () {
+    // Ensure the toolbox is open, and the buttons are all set up.
+    yield this.toolbox.isOpen;
+
     let enabledToolbarButtonsBox = this.panelDoc.getElementById(
       "enabled-toolbox-buttons-box");
 
-    let toggleableButtons = this.toolbox.toolboxButtons;
-    let setToolboxButtonsVisibility =
-      this.toolbox.setToolboxButtonsVisibility.bind(this.toolbox);
+    let toolbarButtons = this.toolbox.toolbarButtons;
+
+    if (!toolbarButtons) {
+      console.warn("The command buttons weren't initiated yet.");
+      return;
+    }
 
     let onCheckboxClick = (checkbox) => {
-      let toolDefinition = toggleableButtons.filter(
+      let commandButton = toolbarButtons.filter(
         toggleableButton => toggleableButton.id === checkbox.id)[0];
       Services.prefs.setBoolPref(
-        toolDefinition.visibilityswitch, checkbox.checked);
-      setToolboxButtonsVisibility();
+        commandButton.visibilityswitch, checkbox.checked);
+      this.toolbox.updateToolboxButtonsVisibility();
     };
 
-    let createCommandCheckbox = tool => {
+    let createCommandCheckbox = button => {
       let checkboxLabel = this.panelDoc.createElement("label");
       let checkboxSpanLabel = this.panelDoc.createElement("span");
-      checkboxSpanLabel.textContent = tool.label;
+      checkboxSpanLabel.textContent = button.description;
       let checkboxInput = this.panelDoc.createElement("input");
       checkboxInput.setAttribute("type", "checkbox");
-      checkboxInput.setAttribute("id", tool.id);
-      if (InfallibleGetBoolPref(tool.visibilityswitch)) {
+      checkboxInput.setAttribute("id", button.id);
+      if (button.isVisible) {
         checkboxInput.setAttribute("checked", true);
       }
       checkboxInput.addEventListener("change",
         onCheckboxClick.bind(this, checkboxInput));
 
       checkboxLabel.appendChild(checkboxInput);
       checkboxLabel.appendChild(checkboxSpanLabel);
       return checkboxLabel;
     };
 
-    for (let tool of toggleableButtons) {
-      if (!tool.isTargetSupported(this.toolbox.target)) {
+    for (let button of toolbarButtons) {
+      if (!button.isTargetSupported(this.toolbox.target)) {
         continue;
       }
 
-      enabledToolbarButtonsBox.appendChild(createCommandCheckbox(tool));
+      enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button));
     }
-  },
+  }),
 
   setupToolsList: function () {
     let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
     let additionalToolsBox = this.panelDoc.getElementById(
       "additional-tools-box");
     let toolsNotSupportedLabel = this.panelDoc.getElementById(
       "tools-not-supported-label");
     let atleastOneToolNotSupported = false;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const MAX_ORDINAL = 99;
 const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
 const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
+const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide";
 const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
 const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
 const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
 const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const { SourceMapService } = require("./source-map-service");
 
 var {Ci, Cu} = require("chrome");
@@ -57,16 +58,18 @@ loader.lazyRequireGetter(this, "getPrefe
 loader.lazyRequireGetter(this, "KeyShortcuts",
   "devtools/client/shared/key-shortcuts");
 loader.lazyRequireGetter(this, "ZoomKeys",
   "devtools/client/shared/zoom-keys");
 loader.lazyRequireGetter(this, "settleAll",
   "devtools/shared/ThreadSafeDevToolsUtils", true);
 loader.lazyRequireGetter(this, "ToolboxButtons",
   "devtools/client/definitions", true);
+loader.lazyRequireGetter(this, "ViewHelpers",
+  "devtools/client/shared/widgets/view-helpers", true);
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/har/toolbox-overlay").register;
 });
 
 /**
  * A "Toolbox" is the component that holds all the tools for one specific
  * target. Visually, it's a document that includes the tools tabs and all
@@ -100,17 +103,17 @@ function Toolbox(target, selectedTool, h
 
   // Map of frames (id => frame-info) and currently selected frame id.
   this.frameMap = new Map();
   this.selectedFrameId = null;
 
   this._toolRegistered = this._toolRegistered.bind(this);
   this._toolUnregistered = this._toolUnregistered.bind(this);
   this._refreshHostTitle = this._refreshHostTitle.bind(this);
-  this._toggleAutohide = this._toggleAutohide.bind(this);
+  this._toggleNoAutohide = this._toggleNoAutohide.bind(this);
   this.showFramesMenu = this.showFramesMenu.bind(this);
   this._updateFrames = this._updateFrames.bind(this);
   this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
   this.destroy = this.destroy.bind(this);
   this.highlighterUtils = getHighlighterUtils(this);
   this._highlighterReady = this._highlighterReady.bind(this);
   this._highlighterHidden = this._highlighterHidden.bind(this);
   this._applyCacheSettings = this._applyCacheSettings.bind(this);
@@ -122,32 +125,36 @@ function Toolbox(target, selectedTool, h
   this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
   this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
   this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
   this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
   this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
   this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
   this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
   this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
-  this._onTabbarFocus = this._onTabbarFocus.bind(this);
-  this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.bind(this);
+  this._onToolbarFocus = this._onToolbarFocus.bind(this);
+  this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this);
   this._onPickerClick = this._onPickerClick.bind(this);
   this._onPickerKeypress = this._onPickerKeypress.bind(this);
   this._onPickerStarted = this._onPickerStarted.bind(this);
   this._onPickerStopped = this._onPickerStopped.bind(this);
+  this.selectTool = this.selectTool.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
   }
   this._defaultToolId = selectedTool;
 
   this._hostType = hostType;
 
+  this._isOpenDeferred = defer();
+  this.isOpen = this._isOpenDeferred.promise;
+
   EventEmitter.decorate(this);
 
   this._target.on("navigate", this._refreshHostTitle);
   this._target.on("frame-update", this._updateFrames);
 
   this.on("host-changed", this._refreshHostTitle);
   this.on("select", this._refreshHostTitle);
 
@@ -175,17 +182,62 @@ Toolbox.HostType = {
 Toolbox.prototype = {
   _URL: "about:devtools-toolbox",
 
   _prefs: {
     LAST_TOOL: "devtools.toolbox.selectedTool",
     SIDE_ENABLED: "devtools.toolbox.sideEnabled",
   },
 
-  currentToolId: null,
+  get currentToolId() {
+    return this._currentToolId;
+  },
+
+  set currentToolId(id) {
+    this._currentToolId = id;
+    this.component.setCurrentToolId(id);
+  },
+
+  get panelDefinitions() {
+    return this._panelDefinitions;
+  },
+
+  set panelDefinitions(definitions) {
+    this._panelDefinitions = definitions;
+    this._combineAndSortPanelDefinitions();
+  },
+
+  get visibleAdditionalTools() {
+    if (!this._visibleAdditionalTools) {
+      this._visibleAdditionalTools = [];
+    }
+
+    return this._visibleAdditionalTools;
+  },
+
+  set visibleAdditionalTools(tools) {
+    this._visibleAdditionalTools = tools;
+    this._combineAndSortPanelDefinitions();
+  },
+
+  /**
+   * Combines the built-in panel definitions and the additional tool definitions that
+   * can be set by add-ons.
+   */
+  _combineAndSortPanelDefinitions() {
+    const definitions = [...this._panelDefinitions, ...this.getVisibleAdditionalTools()];
+    definitions.sort(definition => {
+      return -1 * (definition.ordinal == undefined || definition.ordinal < 0
+        ? MAX_ORDINAL
+        : definition.ordinal
+      );
+    });
+    this.component.setPanelDefinitions(definitions);
+  },
+
   lastUsedToolId: null,
 
   /**
    * Returns a *copy* of the _toolPanels collection.
    *
    * @return {Map} panels
    *         All the running panels in the toolbox
    */
@@ -361,57 +413,47 @@ Toolbox.prototype = {
 
       // Attach the thread
       this._threadClient = yield attachThread(this);
       yield domReady.promise;
 
       this.isReady = true;
       let framesPromise = this._listFrames();
 
-      this.closeButton = this.doc.getElementById("toolbox-close");
-      this.closeButton.addEventListener("click", this.destroy, true);
-
       Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings,
                                 false);
       Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
                                  this._applyServiceWorkersTestingSettings, false);
-      Services.prefs.addObserver("devtools.screenshot.clipboard.enabled",
-                                 this._buildButtons, false);
-
-      let framesMenu = this.doc.getElementById("command-button-frames");
-      framesMenu.addEventListener("click", this.showFramesMenu, false);
-
-      let noautohideMenu = this.doc.getElementById("command-button-noautohide");
-      noautohideMenu.addEventListener("click", this._toggleAutohide, true);
 
       this.textBoxContextMenuPopup =
         this.doc.getElementById("toolbox-textbox-context-popup");
       this.textBoxContextMenuPopup.addEventListener("popupshowing",
         this._updateTextBoxMenuItems, true);
 
       this.shortcuts = new KeyShortcuts({
         window: this.doc.defaultView
       });
+      // Get the DOM element to mount the ToolboxController to.
+      this._componentMount = this.doc.getElementById("toolbox-toolbar-mount");
+
+      this._mountReactComponent();
       this._buildDockButtons();
       this._buildOptions();
       this._buildTabs();
       this._applyCacheSettings();
       this._applyServiceWorkersTestingSettings();
       this._addKeysToWindow();
       this._addReloadKeys();
       this._addHostListeners();
       this._registerOverlays();
       if (!this._hostOptions || this._hostOptions.zoom === true) {
         ZoomKeys.register(this.win);
       }
 
-      this.tabbar = this.doc.querySelector(".devtools-tabbar");
-      this.tabbar.addEventListener("focus", this._onTabbarFocus, true);
-      this.tabbar.addEventListener("click", this._onTabbarFocus, true);
-      this.tabbar.addEventListener("keypress", this._onTabbarArrowKeypress);
+      this._componentMount.addEventListener("keypress", this._onToolbarArrowKeypress);
 
       this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
       this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
       this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
 
       let buttonsPromise = this._buildButtons();
 
       this._pingTelemetry();
@@ -419,16 +461,20 @@ Toolbox.prototype = {
       // The isTargetSupported check needs to happen after the target is
       // remoted, otherwise we could have done it in the toolbox constructor
       // (bug 1072764).
       let toolDef = gDevTools.getToolDefinition(this._defaultToolId);
       if (!toolDef || !toolDef.isTargetSupported(this._target)) {
         this._defaultToolId = "webconsole";
       }
 
+      // Start rendering the toolbox toolbar before selecting the tool, as the tools
+      // can take a few hundred milliseconds seconds to start up.
+      this.component.setCanRender();
+
       yield this.selectTool(this._defaultToolId);
 
       // Wait until the original tool is selected so that the split
       // console input will receive focus.
       let splitConsolePromise = promise.resolve();
       if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
         splitConsolePromise = this.openSplitConsole();
       }
@@ -446,16 +492,17 @@ Toolbox.prototype = {
       // If in testing environment, wait for performance connection to finish,
       // so we don't have to explicitly wait for this in tests; ideally, all tests
       // will handle this on their own, but each have their own tear down function.
       if (flags.testing) {
         yield performanceFrontConnection;
       }
 
       this.emit("ready");
+      this._isOpenDeferred.resolve();
     }.bind(this)).then(null, console.error.bind(console));
   },
 
   /**
    * loading React modules when needed (to avoid performance penalties
    * during Firefox start up time).
    */
   get React() {
@@ -465,16 +512,20 @@ Toolbox.prototype = {
   get ReactDOM() {
     return this.browserRequire("devtools/client/shared/vendor/react-dom");
   },
 
   get ReactRedux() {
     return this.browserRequire("devtools/client/shared/vendor/react-redux");
   },
 
+  get ToolboxController() {
+    return this.browserRequire("devtools/client/framework/components/toolbox-controller");
+  },
+
   // Return HostType id for telemetry
   _getTelemetryHostId: function () {
     switch (this.hostType) {
       case Toolbox.HostType.BOTTOM: return 0;
       case Toolbox.HostType.SIDE: return 1;
       case Toolbox.HostType.WINDOW: return 2;
       case Toolbox.HostType.CUSTOM: return 3;
       default: return 9;
@@ -487,16 +538,61 @@ Toolbox.prototype = {
     this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
     this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
                                              Services.appinfo.is64Bit ? 1 : 0);
     this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
                                              system.getScreenDimensions());
     this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
   },
 
+  /**
+   * Create a simple object to store the state of a toolbox button. The checked state of
+   * a button can be updated arbitrarily outside of the scope of the toolbar and its
+   * controllers. In order to simplify this interaction this object emits an
+   * "updatechecked" event any time the isChecked value is updated, allowing any consuming
+   * components to listen and respond to updates.
+   *
+   * @param {Object} options:
+   *
+   * @property {String} id - The id of the button or command.
+   * @property {String} className - An optional additional className for the button.
+   * @property {String} description - The value that will display as a tooltip and in
+   *                    the options panel for enabling/disabling.
+   * @property {Function} onClick - The function to run when the button is activated by
+   *                      click or keyboard shortcut.
+   * @property {Boolean} isInStartContainer - Buttons can either be placed at the start
+   *                     of the toolbar, or at the end.
+   */
+  _createButtonState: function (options) {
+    let isChecked = false;
+    const {id, className, description, onClick, isInStartContainer} = options;
+    const button = {
+      id,
+      className,
+      description,
+      onClick,
+      get isChecked() {
+        return isChecked;
+      },
+      set isChecked(value) {
+        isChecked = value;
+        this.emit("updatechecked");
+      },
+      // The preference for having this button visible.
+      visibilityswitch: `devtools.${id}.enabled`,
+      // The toolbar has a container at the start and end of the toolbar for
+      // holding buttons. By default the buttons are placed in the end container.
+      isInStartContainer: !!isInStartContainer
+    };
+
+    EventEmitter.decorate(button);
+
+    return button;
+  },
+
   _buildOptions: function () {
     let selectOptions = (name, event) => {
       // Flip back to the last used panel if we are already
       // on the options panel.
       if (this.currentToolId === "options" &&
           gDevTools.getToolDefinition(this.lastUsedToolId)) {
         this.selectTool(this.lastUsedToolId);
       } else {
@@ -749,95 +845,60 @@ Toolbox.prototype = {
     return this._notificationBox;
   },
 
   /**
    * Build the buttons for changing hosts. Called every time
    * the host changes.
    */
   _buildDockButtons: function () {
-    let dockBox = this.doc.getElementById("toolbox-dock-buttons");
-
-    while (dockBox.firstChild) {
-      dockBox.removeChild(dockBox.firstChild);
-    }
-
     if (!this._target.isLocalTab) {
+      this.component.setDockButtonsEnabled(false);
       return;
     }
 
     // Bottom-type host can be minimized, add a button for this.
     if (this.hostType == Toolbox.HostType.BOTTOM) {
-      let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
-      minimizeBtn.id = "toolbox-dock-bottom-minimize";
-      minimizeBtn.className = "devtools-button";
-      /* Bug 1177463 - The minimize button is currently hidden until we agree on
-         the UI for it, and until bug 1173849 is fixed too. */
-      minimizeBtn.setAttribute("hidden", "true");
-
-      minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
-      dockBox.appendChild(minimizeBtn);
-      // Show the button in its maximized state.
-      this._onBottomHostMaximized();
+      this.component.setCanMinimize(true);
 
       // Maximize again when a tool gets selected.
       this.on("before-select", this._onToolSelectWhileMinimized);
       // Maximize and stop listening before the host type changes.
       this.once("host-will-change", this._onBottomHostWillChange);
     }
 
-    if (this.hostType == Toolbox.HostType.WINDOW) {
-      this.closeButton.setAttribute("hidden", "true");
-    } else {
-      this.closeButton.removeAttribute("hidden");
-    }
+    this.component.setDockButtonsEnabled(true);
+    this.component.setCanCloseToolbox(this.hostType !== Toolbox.HostType.WINDOW);
 
     let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
 
+    let hostTypes = [];
     for (let type in Toolbox.HostType) {
       let position = Toolbox.HostType[type];
       if (position == this.hostType ||
           position == Toolbox.HostType.CUSTOM ||
           (!sideEnabled && position == Toolbox.HostType.SIDE)) {
         continue;
       }
 
-      let button = this.doc.createElementNS(HTML_NS, "button");
-      button.id = "toolbox-dock-" + position;
-      button.className = "toolbox-dock-button devtools-button";
-      button.setAttribute("title", L10N.getStr("toolboxDockButtons." +
-                                                  position + ".tooltip"));
-      button.addEventListener("click", this.switchHost.bind(this, position));
+      hostTypes.push({
+        position,
+        switchHost: this.switchHost.bind(this, position)
+      });
+    }
 
-      dockBox.appendChild(button);
-    }
-  },
-
-  _getMinimizeButtonShortcutTooltip: function () {
-    let str = L10N.getStr("toolbox.minimize.key");
-    let key = KeyShortcuts.parseElectronKey(this.win, str);
-    return "(" + KeyShortcuts.stringify(key) + ")";
+    this.component.setHostTypes(hostTypes);
   },
 
   _onBottomHostMinimized: function () {
-    let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
-    btn.className = "minimized";
-
-    btn.setAttribute("title",
-      L10N.getStr("toolboxDockButtons.bottom.maximize") + " " +
-      this._getMinimizeButtonShortcutTooltip());
+    this.component.setMinimizeState("minimized");
   },
 
   _onBottomHostMaximized: function () {
-    let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
-    btn.className = "maximized";
-
-    btn.setAttribute("title",
-      L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
-      this._getMinimizeButtonShortcutTooltip());
+    this.component.setMinimizeState("maximized");
   },
 
   _onToolSelectWhileMinimized: function () {
     this.postMessage({
       name: "maximize-host"
     });
   },
 
@@ -862,157 +923,176 @@ Toolbox.prototype = {
 
   _toggleMinimizeMode: function () {
     if (this.hostType !== Toolbox.HostType.BOTTOM) {
       return;
     }
 
     // Calculate the height to which the host should be minimized so the
     // tabbar is still visible.
-    let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
-                                                                    .height;
+    let toolbarHeight = this._componentMount.getBoxQuads({box: "content"})[0].bounds
+                                                                             .height;
     this.postMessage({
       name: "toggle-minimize-mode",
       toolbarHeight
     });
   },
 
   /**
-   * Add tabs to the toolbox UI for registered tools
+   * Initiate ToolboxTabs React component and all it's properties. Do the initial render.
    */
   _buildTabs: function () {
-    // Build tabs for global registered tools.
-    for (let definition of gDevTools.getToolDefinitionArray()) {
-      this._buildTabForTool(definition);
-    }
+    // Get the initial list of tab definitions. This list can be amended at a later time
+    // by tools registering themselves.
+    const definitions = gDevTools.getToolDefinitionArray();
+    definitions.forEach(definition => this._buildPanelForTool(definition));
+
+    // Get the definitions that will only affect the main tab area.
+    this.panelDefinitions = definitions.filter(definition =>
+      definition.isTargetSupported(this._target) && definition.id !== "options");
+
+    this.optionsDefinition = definitions.find(({id}) => id === "options");
+    // The options tool is treated slightly differently, and is in a different area.
+    this.component.setOptionsPanel(definitions.find(({id}) => id === "options"));
   },
 
-  /**
-   * Get all dev tools tab bar focusable elements. These are visible elements
-   * such as buttons or elements with tabindex.
-   */
-  get tabbarFocusableElms() {
-    return [...this.tabbar.querySelectorAll(
-      "[tabindex]:not([hidden]), button:not([hidden])")];
+  _mountReactComponent: function () {
+    // Ensure the toolbar doesn't try to render until the tool is ready.
+    const element = this.React.createElement(this.ToolboxController, {
+      L10N,
+      currentToolId: this.currentToolId,
+      selectTool: this.selectTool,
+      closeToolbox: this.destroy,
+      focusButton: this._onToolbarFocus,
+      toggleMinimizeMode: this._toggleMinimizeMode,
+    });
+
+    this.component = this.ReactDOM.render(element, this._componentMount);
   },
 
   /**
-   * Reset tabindex attributes across all focusable elements inside the tabbar.
+   * Reset tabindex attributes across all focusable elements inside the toolbar.
    * Only have one element with tabindex=0 at a time to make sure that tabbing
-   * results in navigating away from the tabbar container.
+   * results in navigating away from the toolbar container.
    * @param  {FocusEvent} event
    */
-  _onTabbarFocus: function (event) {
-    this.tabbarFocusableElms.forEach(elm =>
-      elm.setAttribute("tabindex", event.target === elm ? "0" : "-1"));
+  _onToolbarFocus: function (id) {
+    this.component.setFocusedButton(id);
   },
 
   /**
-   * On left/right arrow press, attempt to move the focus inside the tabbar to
-   * the previous/next focusable element.
+   * On left/right arrow press, attempt to move the focus inside the toolbar to
+   * the previous/next focusable element. This is not in the React component
+   * as it is difficult to coordinate between different component elements.
+   * The components are responsible for setting the correct tabindex value
+   * for if they are the focused element.
    * @param  {KeyboardEvent} event
    */
-  _onTabbarArrowKeypress: function (event) {
+  _onToolbarArrowKeypress: function (event) {
     let { key, target } = event;
-    let focusableElms = this.tabbarFocusableElms;
-    let curIndex = focusableElms.indexOf(target);
+    let buttons = [...this._componentMount.querySelectorAll("button")];
+    let curIndex = buttons.indexOf(target);
 
     if (curIndex === -1) {
       console.warn(target + " is not found among Developer Tools tab bar " +
-        "focusable elements. It needs to either be a button or have " +
-        "tabindex. If it is intended to be hidden, 'hidden' attribute must " +
-        "be used.");
+        "focusable elements.");
       return;
     }
 
     let newTarget;
 
     if (key === "ArrowLeft") {
       // Do nothing if already at the beginning.
       if (curIndex === 0) {
         return;
       }
-      newTarget = focusableElms[curIndex - 1];
+      newTarget = buttons[curIndex - 1];
     } else if (key === "ArrowRight") {
       // Do nothing if already at the end.
-      if (curIndex === focusableElms.length - 1) {
+      if (curIndex === buttons.length - 1) {
         return;
       }
-      newTarget = focusableElms[curIndex + 1];
+      newTarget = buttons[curIndex + 1];
     } else {
       return;
     }
 
-    focusableElms.forEach(elm =>
-      elm.setAttribute("tabindex", newTarget === elm ? "0" : "-1"));
     newTarget.focus();
 
     event.preventDefault();
     event.stopPropagation();
   },
 
   /**
    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
    */
-  _buildButtons: function () {
-    if (this.target.getTrait("highlightable")) {
-      this._buildPickerButton();
-    }
-
-    this.setToolboxButtonsVisibility();
+  _buildButtons: Task.async(function* () {
+    // Beyond the normal preference filtering
+    this.toolbarButtons = [
+      this._buildPickerButton(),
+      this._buildFrameButton(),
+      yield this._buildNoAutoHideButton()
+    ];
 
-    // Old servers don't have a GCLI Actor, so just return
-    if (!this.target.hasActor("gcli")) {
-      return promise.resolve();
-    }
-    // Disable gcli in browser toolbox until there is usages of it
-    if (this.target.chrome) {
-      return promise.resolve();
+    // Build buttons from the GCLI commands only if the GCLI actor is supported and the
+    // target isn't chrome.
+    if (this.target.hasActor("gcli") && !this.target.chrome) {
+      const options = {
+        environment: CommandUtils.createEnvironment(this, "_target")
+      };
+
+      this._requisition = yield CommandUtils.createRequisition(this.target, options);
+      const spec = this.getToolbarSpec();
+      const commandButtons = yield CommandUtils.createCommandButtons(
+        spec, this.target, this.doc, this._requisition, this._createButtonState);
+      this.toolbarButtons = [...this.toolbarButtons, ...commandButtons];
     }
 
-    const options = {
-      environment: CommandUtils.createEnvironment(this, "_target")
-    };
+    // Mutate the objects here with their visibility.
+    this.toolbarButtons.forEach(command => {
+      const definition = ToolboxButtons.find(t => t.id === command.id);
+      command.isTargetSupported = definition.isTargetSupported
+        ? definition.isTargetSupported
+        : target => target.isLocalTab;
+      command.isVisible = this._commandIsVisible(command.id);
+    });
 
-    return CommandUtils.createRequisition(this.target, options).then(requisition => {
-      this._requisition = requisition;
+    this.component.setToolboxButtons(this.toolbarButtons);
+  }),
 
-      let spec = this.getToolbarSpec();
-      return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
-        .then(buttons => {
-          let container = this.doc.getElementById("toolbox-buttons");
-          buttons.forEach(button => {
-            let currentButton = this.doc.getElementById(button.id);
-            if (currentButton) {
-              container.replaceChild(button, currentButton);
-            } else {
-              container.appendChild(button);
-            }
-          });
-          this.setToolboxButtonsVisibility();
-        });
+  /**
+   * Button to select a frame for the inspector to target.
+   */
+  _buildFrameButton() {
+    this.frameButton = this._createButtonState({
+      id: "command-button-frames",
+      description: L10N.getStr("toolbox.frames.tooltip"),
+      onClick: this.showFramesMenu
     });
+
+    return this.frameButton;
   },
 
   /**
-   * Adding the element picker button is done here unlike the other buttons
-   * since we want it to work for remote targets too
+   * Button that disables/enables auto-hiding XUL pop-ups. When enabled, XUL
+   * pop-ups will not automatically close when they lose focus.
    */
-  _buildPickerButton: function () {
-    this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
-    this._pickerButton.id = "command-button-pick";
-    this._pickerButton.className =
-      "command-button command-button-invertable devtools-button";
-    this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
+  _buildNoAutoHideButton: Task.async(function* () {
+    this.autohideButton = this._createButtonState({
+      id: "command-button-noautohide",
+      description: L10N.getStr("toolbox.noautohide.tooltip"),
+      onClick: this._toggleNoAutohide
+    });
 
-    let container = this.doc.querySelector("#toolbox-picker-container");
-    container.appendChild(this._pickerButton);
+    this._isDisableAutohideEnabled().then(enabled => {
+      this.autohideButton.isChecked = enabled;
+    });
 
-    this._pickerButton.addEventListener("click", this._onPickerClick, false);
-  },
+    return this.autohideButton;
+  }),
 
   /**
    * Toggle the picker, but also decide whether or not the highlighter should
    * focus the window. This is only desirable when the toolbox is mounted to the
    * window. When devtools is free floating, then the target window should not
    * pop in front of the viewer when the picker is clicked.
    */
   _onPickerClick: function () {
@@ -1037,16 +1117,31 @@ Toolbox.prototype = {
     this.doc.addEventListener("keypress", this._onPickerKeypress, true);
   },
 
   _onPickerStopped: function () {
     this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
   },
 
   /**
+   * The element picker button enables the ability to select a DOM node by clicking
+   * it on the page.
+   */
+  _buildPickerButton() {
+    this.pickerButton = this._createButtonState({
+      id: "command-button-pick",
+      description: L10N.getStr("pickButton.tooltip"),
+      onClick: this._onPickerClick,
+      isInStartContainer: true
+    });
+
+    return this.pickerButton;
+  },
+
+  /**
    * Apply the current cache setting from devtools.cache.disabled to this
    * toolbox's tab.
    */
   _applyCacheSettings: function () {
     let pref = "devtools.cache.disabled";
     let cacheDisabled = Services.prefs.getBoolPref(pref);
 
     if (this.target.activeTab) {
@@ -1065,201 +1160,101 @@ Toolbox.prototype = {
 
     if (this.target.activeTab) {
       this.target.activeTab.reconfigure({
         "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
       });
     }
   },
 
-  /**
-   * Get the toolbar spec for toolbox
-   */
+ /**
+  * Get the toolbar spec for toolbox
+  */
   getToolbarSpec: function () {
     let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
     // Special case for screenshot command button to check for clipboard preference
     const clipboardEnabled = Services.prefs
       .getBoolPref("devtools.screenshot.clipboard.enabled");
     if (clipboardEnabled) {
       for (let i = 0; i < spec.length; i++) {
         if (spec[i] == "screenshot --fullpage --file") {
           spec[i] += " --clipboard";
         }
       }
     }
     return spec;
   },
 
+ /**
+  * Return all toolbox buttons (command buttons, plus any others that were
+  * added manually).
+
   /**
-   * Setter for the checked state of the picker button in the toolbar
-   * @param {Boolean} isChecked
+   * Update the visibility of the buttons.
    */
-  set pickerButtonChecked(isChecked) {
-    if (isChecked) {
-      this._pickerButton.setAttribute("checked", "true");
-    } else {
-      this._pickerButton.removeAttribute("checked");
-    }
+  updateToolboxButtonsVisibility() {
+    this.toolbarButtons.forEach(command => {
+      command.isVisible = this._commandIsVisible(command.id);
+    });
+    this.component.setToolboxButtons(this.toolbarButtons);
   },
 
   /**
-   * Return all toolbox buttons (command buttons, plus any others that were
-   * added manually).
+   * Ensure the visibility of each toolbox button matches the preference value.
    */
-  get toolboxButtons() {
-    return ToolboxButtons.map(options => {
-      let button = this.doc.getElementById(options.id);
-      // Some buttons may not exist inside of Browser Toolbox
-      if (!button) {
-        return false;
-      }
+  _commandIsVisible: function (id) {
+    const {
+      isTargetSupported,
+      visibilityswitch
+    } = this.toolbarButtons.find(btn => btn.id === id);
 
-      return {
-        id: options.id,
-        button: button,
-        label: button.getAttribute("title"),
-        visibilityswitch: "devtools." + options.id + ".enabled",
-        isTargetSupported: options.isTargetSupported
-                           ? options.isTargetSupported
-                           : target => target.isLocalTab,
-      };
-    }).filter(button=>button);
+    let visible = true;
+    try {
+      visible = Services.prefs.getBoolPref(visibilityswitch);
+    } catch (ex) {
+      // Do nothing.
+    }
+
+    if (isTargetSupported) {
+      return visible && isTargetSupported(this.target);
+    }
+
+    return visible;
   },
 
   /**
-   * Ensure the visibility of each toolbox button matches the
-   * preference value.  Simply hide buttons that are preffed off.
-   */
-  setToolboxButtonsVisibility: function () {
-    this.toolboxButtons.forEach(buttonSpec => {
-      let { visibilityswitch, button, isTargetSupported } = buttonSpec;
-      let on = true;
-      try {
-        on = Services.prefs.getBoolPref(visibilityswitch);
-      } catch (ex) {
-        // Do nothing.
-      }
-
-      on = on && isTargetSupported(this.target);
-
-      if (button) {
-        if (on) {
-          button.removeAttribute("hidden");
-        } else {
-          button.setAttribute("hidden", "true");
-        }
-      }
-    });
-
-    this._updateNoautohideButton();
-  },
-
-  /**
-   * Build a tab for one tool definition and add to the toolbox
+   * Build a panel for a tool definition.
    *
    * @param {string} toolDefinition
    *        Tool definition of the tool to build a tab for.
    */
-  _buildTabForTool: function (toolDefinition) {
+  _buildPanelForTool: function (toolDefinition) {
     if (!toolDefinition.isTargetSupported(this._target)) {
       return;
     }
 
-    let tabs = this.doc.getElementById("toolbox-tabs");
     let deck = this.doc.getElementById("toolbox-deck");
-
     let id = toolDefinition.id;
 
     if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
       toolDefinition.ordinal = MAX_ORDINAL;
     }
 
-    let radio = this.doc.createElement("radio");
-    // The radio element is not being used in the conventional way, thus
-    // the devtools-tab class replaces the radio XBL binding with its base
-    // binding (the control-item binding).
-    radio.className = "devtools-tab";
-    radio.id = "toolbox-tab-" + id;
-    radio.setAttribute("toolid", id);
-    radio.setAttribute("tabindex", "0");
-    radio.setAttribute("ordinal", toolDefinition.ordinal);
-    radio.setAttribute("tooltiptext", toolDefinition.tooltip);
-    if (toolDefinition.invertIconForLightTheme) {
-      radio.setAttribute("icon-invertable", "light-theme");
-    } else if (toolDefinition.invertIconForDarkTheme) {
-      radio.setAttribute("icon-invertable", "dark-theme");
-    }
-
-    radio.addEventListener("command", this.selectTool.bind(this, id));
-
-    // spacer lets us center the image and label, while allowing cropping
-    let spacer = this.doc.createElement("spacer");
-    spacer.setAttribute("flex", "1");
-    radio.appendChild(spacer);
-
-    if (toolDefinition.icon) {
-      let image = this.doc.createElement("image");
-      image.className = "default-icon";
-      image.setAttribute("src",
-                         toolDefinition.icon || toolDefinition.highlightedicon);
-      radio.appendChild(image);
-      // Adding the highlighted icon image
-      image = this.doc.createElement("image");
-      image.className = "highlighted-icon";
-      image.setAttribute("src",
-                         toolDefinition.highlightedicon || toolDefinition.icon);
-      radio.appendChild(image);
-    }
-
-    if (toolDefinition.label && !toolDefinition.iconOnly) {
-      let label = this.doc.createElement("label");
-      label.setAttribute("value", toolDefinition.label);
-      label.setAttribute("crop", "end");
-      label.setAttribute("flex", "1");
-      radio.appendChild(label);
-    }
-
     if (!toolDefinition.bgTheme) {
       toolDefinition.bgTheme = "theme-toolbar";
     }
-    let vbox = this.doc.createElement("vbox");
-    vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
+    let panel = this.doc.createElement("vbox");
+    panel.className = "toolbox-panel " + toolDefinition.bgTheme;
 
     // There is already a container for the webconsole frame.
     if (!this.doc.getElementById("toolbox-panel-" + id)) {
-      vbox.id = "toolbox-panel-" + id;
+      panel.id = "toolbox-panel-" + id;
     }
 
-    if (id === "options") {
-      // Options panel is special.  It doesn't belong in the same container as
-      // the other tabs.
-      radio.setAttribute("role", "button");
-      let optionTabContainer = this.doc.getElementById("toolbox-option-container");
-      optionTabContainer.appendChild(radio);
-      deck.appendChild(vbox);
-    } else {
-      radio.setAttribute("role", "tab");
-
-      // If there is no tab yet, or the ordinal to be added is the largest one.
-      if (tabs.childNodes.length == 0 ||
-          tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
-        tabs.appendChild(radio);
-        deck.appendChild(vbox);
-      } else {
-        // else, iterate over all the tabs to get the correct location.
-        Array.some(tabs.childNodes, (node, i) => {
-          if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
-            tabs.insertBefore(radio, node);
-            deck.insertBefore(vbox, deck.childNodes[i]);
-            return true;
-          }
-          return false;
-        });
-      }
-    }
+    deck.appendChild(panel);
 
     this._addKeysToWindow();
   },
 
   /**
    * Lazily created map of the additional tools registered to this toolbox.
    *
    * @returns {Map<string, object>}
@@ -1277,17 +1272,31 @@ Toolbox.prototype = {
 
   /**
    * Retrieve the array of the additional tools registered to this toolbox.
    *
    * @return {Array<object>}
    *         the array of additional tool definitions registered on this toolbox.
    */
   getAdditionalTools() {
-    return Array.from(this.additionalToolDefinitions.values());
+    if (this._additionalToolDefinitions) {
+      return Array.from(this.additionalToolDefinitions.values());
+    }
+    return [];
+  },
+
+  /**
+   * Get the additional tools that have been registered and are visible.
+   *
+   * @return {Array<object>}
+   *         the array of additional tool definitions registered on this toolbox.
+   */
+  getVisibleAdditionalTools() {
+    return this.visibleAdditionalTools
+               .map(toolId => this.additionalToolDefinitions.get(toolId));
   },
 
   /**
    * Test the existence of a additional tools registered to this toolbox by tool id.
    *
    * @param {string} toolId
    *        the id of the tool to test for existence.
    *
@@ -1308,35 +1317,39 @@ Toolbox.prototype = {
     if (!definition.id) {
       throw new Error("Tool definition id is missing");
     }
 
     if (this.isToolRegistered(definition.id)) {
       throw new Error("Tool definition already registered: " +
                       definition.id);
     }
+    this.additionalToolDefinitions.set(definition.id, definition);
+    this.visibleAdditionalTools = [...this.visibleAdditionalTools, definition.id];
 
-    this.additionalToolDefinitions.set(definition.id, definition);
-    this._buildTabForTool(definition);
+    this._combineAndSortPanelDefinitions();
+    this._buildPanelForTool(definition);
   },
 
   /**
    * Unregister and unload an additional tool from this particular toolbox.
    *
    * @param {string} toolId
    *        the id of the additional tool to unregister and remove.
    */
   removeAdditionalTool(toolId) {
     if (!this.hasAdditionalTool(toolId)) {
       throw new Error("Tool definition not registered to this toolbox: " +
                       toolId);
     }
 
+    this.additionalToolDefinitions.delete(toolId);
+    this.visibleAdditionalTools = this.visibleAdditionalTools
+                                      .filter(id => id !== toolId);
     this.unloadTool(toolId);
-    this.additionalToolDefinitions.delete(toolId);
   },
 
   /**
    * Ensure the tool with the given id is loaded.
    *
    * @param {string} id
    *        The id of the tool to load.
    */
@@ -1380,16 +1393,17 @@ Toolbox.prototype = {
 
     gDevTools.emit(id + "-init", this, iframe);
     this.emit(id + "-init", iframe);
 
     // If no parent yet, append the frame into default location.
     if (!iframe.parentNode) {
       let vbox = this.doc.getElementById("toolbox-panel-" + id);
       vbox.appendChild(iframe);
+      vbox.visibility = "visible";
     }
 
     let onLoad = () => {
       // Prevent flicker while loading by waiting to make visible until now.
       iframe.style.visibility = "visible";
 
       // Try to set the dir attribute as early as possible.
       this.setIframeDocumentDir(iframe);
@@ -1517,28 +1531,16 @@ Toolbox.prototype = {
    * Switch to the tool with the given id
    *
    * @param {string} id
    *        The id of the tool to switch to
    */
   selectTool: function (id) {
     this.emit("before-select", id);
 
-    let tabs = this.doc.querySelectorAll(".devtools-tab");
-    this.selectSingleNode(tabs, "toolbox-tab-" + id);
-
-    // If options is selected, the separator between it and the
-    // command buttons should be hidden.
-    let sep = this.doc.getElementById("toolbox-controls-separator");
-    if (id === "options") {
-      sep.setAttribute("invisible", "true");
-    } else {
-      sep.removeAttribute("invisible");
-    }
-
     if (this.currentToolId == id) {
       let panel = this._toolPanels.get(id);
       if (panel) {
         // We have a panel instance, so the tool is already fully loaded.
 
         // re-focus tool to get key events again
         this.focusTool(id);
 
@@ -1549,33 +1551,28 @@ Toolbox.prototype = {
       // so we are racing another call to selectTool with the same id.
       return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
     }
 
     if (!this.isReady) {
       throw new Error("Can't select tool, wait for toolbox 'ready' event");
     }
 
-    let tab = this.doc.getElementById("toolbox-tab-" + id);
-
-    if (tab) {
+    // Check if the tool exists.
+    if (this.panelDefinitions.find((definition) => definition.id === id) ||
+        id === "options" ||
+        this.additionalToolDefinitions.get(id)) {
       if (this.currentToolId) {
         this._telemetry.toolClosed(this.currentToolId);
       }
       this._telemetry.toolOpened(id);
     } else {
       throw new Error("No tool found");
     }
 
-    let tabstrip = this.doc.getElementById("toolbox-tabs");
-
-    // select the right tab, making 0th index the default tab if right tab not
-    // found.
-    tabstrip.selectedItem = tab || tabstrip.childNodes[0];
-
     // and select the right iframe
     let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
     this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
 
     this.lastUsedToolId = this.currentToolId;
     this.currentToolId = id;
     this._refreshConsoleDisplay();
     if (id != "options") {
@@ -1639,19 +1636,19 @@ Toolbox.prototype = {
    *
    * @returns {Promise} a promise that resolves once the tool has been
    *          loaded and focused.
    */
   openSplitConsole: function () {
     this._splitConsole = true;
     Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
     this._refreshConsoleDisplay();
-    this.emit("split-console");
 
     return this.loadTool("webconsole").then(() => {
+      this.emit("split-console");
       this.focusConsoleInput();
     });
   },
 
   /**
    * Closes the split console.
    *
    * @returns {Promise} a promise that resolves once the tool has been
@@ -1692,57 +1689,65 @@ Toolbox.prototype = {
   reloadTarget: function (force) {
     this.target.activeTab.reload({ force: force });
   },
 
   /**
    * Loads the tool next to the currently selected tool.
    */
   selectNextTool: function () {
-    let tools = this.doc.querySelectorAll(".devtools-tab");
-    let selected = this.doc.querySelector(".devtools-tab[selected]");
-    let nextIndex = [...tools].indexOf(selected) + 1;
-    let next = tools[nextIndex] || tools[0];
-    let tool = next.getAttribute("toolid");
-    return this.selectTool(tool);
+    const index = this.panelDefinitions.findIndex(({id}) => id === this.currentToolId);
+    let definition = this.panelDefinitions[index + 1];
+    if (!definition) {
+      definition = index === -1
+        ? this.panelDefinitions[0]
+        : this.optionsDefinition;
+    }
+    return this.selectTool(definition.id);
   },
 
   /**
    * Loads the tool just left to the currently selected tool.
    */
   selectPreviousTool: function () {
-    let tools = this.doc.querySelectorAll(".devtools-tab");
-    let selected = this.doc.querySelector(".devtools-tab[selected]");
-    let prevIndex = [...tools].indexOf(selected) - 1;
-    let prev = tools[prevIndex] || tools[tools.length - 1];
-    let tool = prev.getAttribute("toolid");
-    return this.selectTool(tool);
+    const index = this.panelDefinitions.findIndex(({id}) => id === this.currentToolId);
+    let definition = this.panelDefinitions[index - 1];
+    if (!definition) {
+      definition = index === -1
+        ? this.panelDefinitions[this.panelDefinitions.length - 1]
+        : this.optionsDefinition;
+    }
+    return this.selectTool(definition.id);
   },
 
   /**
    * Highlights the tool's tab if it is not the currently selected tool.
    *
    * @param {string} id
    *        The id of the tool to highlight
    */
-  highlightTool: function (id) {
-    let tab = this.doc.getElementById("toolbox-tab-" + id);
-    tab && tab.setAttribute("highlighted", "true");
-  },
+  highlightTool: Task.async(function* (id) {
+    if (!this.component) {
+      yield this.isOpen;
+    }
+    this.component.highlightTool(id);
+  }),
 
   /**
    * De-highlights the tool's tab.
    *
    * @param {string} id
    *        The id of the tool to unhighlight
    */
-  unhighlightTool: function (id) {
-    let tab = this.doc.getElementById("toolbox-tab-" + id);
-    tab && tab.removeAttribute("highlighted");
-  },
+  unhighlightTool: Task.async(function* (id) {
+    if (!this.component) {
+      yield this.isOpen;
+    }
+    this.component.unhighlightTool(id);
+  }),
 
   /**
    * Raise the toolbox host.
    */
   raise: function () {
     this.postMessage({
       name: "raise-host"
     });
@@ -1762,46 +1767,45 @@ Toolbox.prototype = {
     this.postMessage({
       name: "set-host-title",
       title
     });
   },
 
   // Returns an instance of the preference actor
   get _preferenceFront() {
-    return this.target.root.then(rootForm => {
-      return getPreferenceFront(this.target.client, rootForm);
+    return this.isOpen.then(() => {
+      return this.target.root.then(rootForm => {
+        return getPreferenceFront(this.target.client, rootForm);
+      });
     });
   },
 
-  _toggleAutohide: Task.async(function* () {
-    let prefName = "ui.popup.disable_autohide";
+  _toggleNoAutohide: Task.async(function* () {
     let front = yield this._preferenceFront;
-    let current = yield front.getBoolPref(prefName);
-    yield front.setBoolPref(prefName, !current);
+    let toggledValue = !(yield this._isDisableAutohideEnabled(front));
 
-    this._updateNoautohideButton();
+    front.setBoolPref(DISABLE_AUTOHIDE_PREF, toggledValue);
+
+    this.autohideButton.isChecked = toggledValue;
   }),
 
-  _updateNoautohideButton: Task.async(function* () {
-    let menu = this.doc.getElementById("command-button-noautohide");
-    if (menu.getAttribute("hidden") === "true") {
-      return;
-    }
-    if (!this.target.root) {
-      return;
+  _isDisableAutohideEnabled: Task.async(function* (prefFront) {
+    // Ensure that the tools are open, and the button is visible.
+    yield this.isOpen;
+    if (!this.autohideButton.isVisible) {
+      return false;
     }
-    let prefName = "ui.popup.disable_autohide";
-    let front = yield this._preferenceFront;
-    let current = yield front.getBoolPref(prefName);
-    if (current) {
-      menu.setAttribute("checked", "true");
-    } else {
-      menu.removeAttribute("checked");
+
+    // If no prefFront was provided, then get one.
+    if (!prefFront) {
+      prefFront = yield this._preferenceFront;
     }
+
+    return yield prefFront.getBoolPref(DISABLE_AUTOHIDE_PREF);
   }),
 
   _listFrames: function (event) {
     if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
       // We are not targetting a regular TabActor
       // it can be either an addon or browser toolbox actor
       return promise.resolve();
     }
@@ -1833,21 +1837,21 @@ Toolbox.prototype = {
         checked,
         click: () => {
           this.onSelectFrame(frame.id);
         }
       }));
     });
 
     menu.once("open").then(() => {
-      target.setAttribute("open", "true");
+      this.frameButton.isChecked = true;
     });
 
     menu.once("close").then(() => {
-      target.removeAttribute("open");
+      this.frameButton.isChecked = false;
     });
 
     // Show a drop down menu with frames.
     // XXX Missing menu API for specifying target (anchor)
     // and relative position to it. See also:
     // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
     let rect = target.getBoundingClientRect();
@@ -1925,23 +1929,22 @@ Toolbox.prototype = {
       let topFrames = frames.filter(frame => !frame.parentID);
       this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
     }
 
     // Check out whether top frame is currently selected.
     // Note that only child frame has parentID.
     let frame = this.frameMap.get(this.selectedFrameId);
     let topFrameSelected = frame ? !frame.parentID : false;
-    let button = this.doc.getElementById("command-button-frames");
-    button.removeAttribute("checked");
+    this._framesButtonChecked = false;
 
     // If non-top level frame is selected the toolbar button is
     // marked as 'checked' indicating that a child frame is active.
     if (!topFrameSelected && this.selectedFrameId) {
-      button.setAttribute("checked", "true");
+      this._framesButtonChecked = false;
     }
   },
 
   /**
    * Switch to the last used host for the toolbox UI.
    */
   switchToPreviousHost: function () {
     return this.switchHost("previous");
@@ -2035,35 +2038,42 @@ Toolbox.prototype = {
     }
 
     if (this._toolPanels.has(toolId)) {
       let instance = this._toolPanels.get(toolId);
       instance.destroy();
       this._toolPanels.delete(toolId);
     }
 
-    let radio = this.doc.getElementById("toolbox-tab-" + toolId);
     let panel = this.doc.getElementById("toolbox-panel-" + toolId);
 
-    if (radio) {
-      if (this.currentToolId == toolId) {
-        let nextToolName = null;
-        if (radio.nextSibling) {
-          nextToolName = radio.nextSibling.getAttribute("toolid");
-        }
-        if (radio.previousSibling) {
-          nextToolName = radio.previousSibling.getAttribute("toolid");
-        }
-        if (nextToolName) {
-          this.selectTool(nextToolName);
-        }
+    // Select another tool.
+    if (this.currentToolId == toolId) {
+      let index = this.panelDefinitions.findIndex(({id}) => id === toolId);
+      let nextTool = this.panelDefinitions[index + 1];
+      let previousTool = this.panelDefinitions[index - 1];
+      let toolNameToSelect;
+
+      if (nextTool) {
+        toolNameToSelect = nextTool.id;
       }
-      radio.parentNode.removeChild(radio);
+      if (previousTool) {
+        toolNameToSelect = previousTool.id;
+      }
+      if (toolNameToSelect) {
+        this.selectTool(toolNameToSelect);
+      }
     }
 
+    // Remove this tool from the current panel definitions.
+    this.panelDefinitions = this.panelDefinitions.filter(({id}) => id !== toolId);
+    this.visibleAdditionalTools = this.visibleAdditionalTools
+                                      .filter(id => id !== toolId);
+    this._combineAndSortPanelDefinitions();
+
     if (panel) {
       panel.parentNode.removeChild(panel);
     }
 
     if (this.hostType == Toolbox.HostType.WINDOW) {
       let doc = this.win.parent.document;
       let key = doc.getElementById("key_" + toolId);
       if (key) {
@@ -2075,27 +2085,38 @@ Toolbox.prototype = {
   /**
    * Handler for the tool-registered event.
    * @param  {string} event
    *         Name of the event ("tool-registered")
    * @param  {string} toolId
    *         Id of the tool that was registered
    */
   _toolRegistered: function (event, toolId) {
-    let tool = this.getToolDefinition(toolId);
-    if (!tool) {
-      // Ignore if the tool is not found, when a per-toolbox tool
-      // has been toggle in the toolbox options view, every toolbox will receive
-      // the toolbox-register and toolbox-unregister events.
-      return;
+    // Tools can either be in the global devtools, or added to this specific toolbox
+    // as an additional tool.
+    let definition = gDevTools.getToolDefinition(toolId);
+    let isAdditionalTool = false;
+    if (!definition) {
+      definition = this.additionalToolDefinitions.get(toolId);
+      isAdditionalTool = true;
     }
-    this._buildTabForTool(tool);
-    // Emit the event so tools can listen to it from the toolbox level
-    // instead of gDevTools.
-    this.emit("tool-registered", toolId);
+
+    if (definition.isTargetSupported(this._target)) {
+      if (isAdditionalTool) {
+        this.visibleAdditionalTools = [...this.visibleAdditionalTools, toolId];
+        this._combineAndSortPanelDefinitions();
+      } else {
+        this.panelDefinitions = this.panelDefinitions.concat(definition);
+      }
+      this._buildPanelForTool(definition);
+
+      // Emit the event so tools can listen to it from the toolbox level
+      // instead of gDevTools.
+      this.emit("tool-registered", toolId);
+    }
   },
 
   /**
    * Handler for the tool-unregistered event.
    * @param  {string} event
    *         Name of the event ("tool-unregistered")
    * @param  {string} toolId
    *         id of the tool that was unregistered
@@ -2141,16 +2162,20 @@ Toolbox.prototype = {
       return this._destroyingInspector;
     }
 
     this._destroyingInspector = Task.spawn(function* () {
       if (!this._inspector) {
         return;
       }
 
+      // Ensure that the inspector isn't still being initiated, otherwise race conditions
+      // in the initialization process can throw errors.
+      yield this._initInspector;
+
       // Releasing the walker (if it has been created)
       // This can fail, but in any case, we want to continue destroying the
       // inspector/highlighter/selection
       // FF42+: Inspector actor starts managing Walker actor and auto destroy it.
       if (this._walker && !this.walker.traits.autoReleased) {
         try {
           yield this._walker.release();
         } catch (e) {
@@ -2231,30 +2256,25 @@ Toolbox.prototype = {
     }
 
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
         this._saveSplitConsoleHeight);
       this.webconsolePanel = null;
     }
-    if (this.closeButton) {
-      this.closeButton.removeEventListener("click", this.destroy, true);
-      this.closeButton = null;
-    }
     if (this.textBoxContextMenuPopup) {
       this.textBoxContextMenuPopup.removeEventListener("popupshowing",
         this._updateTextBoxMenuItems, true);
       this.textBoxContextMenuPopup = null;
     }
-    if (this.tabbar) {
-      this.tabbar.removeEventListener("focus", this._onTabbarFocus, true);
-      this.tabbar.removeEventListener("click", this._onTabbarFocus, true);
-      this.tabbar.removeEventListener("keypress", this._onTabbarArrowKeypress);
-      this.tabbar = null;
+    if (this._componentMount) {
+      this._componentMount.removeEventListener("keypress", this._onToolbarArrowKeypress);
+      this.ReactDOM.unmountComponentAtNode(this._componentMount);
+      this._componentMount = null;
     }
 
     let outstanding = [];
     for (let [id, panel] of this._toolPanels) {
       try {
         gDevTools.emit(id + "-destroy", this, panel);
         this.emit(id + "-destroy", panel);
 
@@ -2273,27 +2293,24 @@ Toolbox.prototype = {
     if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
       this.target.activeTab.reconfigure({
         "cacheDisabled": false,
         "serviceWorkersTestingEnabled": false
       });
     }
 
     // Destroying the walker and inspector fronts
-    outstanding.push(this.destroyInspector().then(() => {
-      // Removing buttons
-      if (this._pickerButton) {
-        this._pickerButton.removeEventListener("click", this._togglePicker, false);
-        this._pickerButton = null;
-      }
-    }));
+    outstanding.push(this.destroyInspector());
 
     // Destroy the profiler connection
     outstanding.push(this.destroyPerformance());
 
+    // Destroy the preference front
+    outstanding.push(this.destroyPreference());
+
     // Detach the thread
     detachThread(this._threadClient);
     this._threadClient = null;
 
     // We need to grab a reference to win before this._host is destroyed.
     let win = this.win;
 
     if (this._requisition) {
@@ -2447,16 +2464,24 @@ Toolbox.prototype = {
       yield this._performanceFrontConnection.promise;
     }
     this.performance.off("*", this._onPerformanceFrontEvent);
     yield this.performance.destroy();
     this._performance = null;
   }),
 
   /**
+   * Destroy the preferences actor when the toolbox is unloaded.
+   */
+  destroyPreference: Task.async(function* () {
+    let front = yield this._preferenceFront;
+    front.destroy();
+  }),
+
+  /**
    * Called when any event comes from the PerformanceFront. If the performance tool is
    * already loaded when the first event comes in, immediately unbind this handler, as
    * this is only used to queue up observed recordings before the performance tool can
    * handle them, which will only occur when `console.profile()` recordings are started
    * before the tool loads.
    */
   _onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
     if (this.getPanel("performance")) {
--- a/devtools/client/framework/toolbox.xul
+++ b/devtools/client/framework/toolbox.xul
@@ -42,38 +42,17 @@
       <menuitem id="cMenu_delete"/>
       <menuseparator/>
       <menuitem id="cMenu_selectAll"/>
     </menupopup>
   </popupset>
 
   <vbox id="toolbox-container" flex="1">
     <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-notificationbox"/>
-    <toolbar class="devtools-tabbar">
-      <hbox id="toolbox-picker-container" />
-      <hbox id="toolbox-tabs" flex="1" role="tablist" />
-      <hbox id="toolbox-buttons" pack="end">
-        <html:button id="command-button-frames"
-                     class="command-button command-button-invertable devtools-button"
-                     title="&toolboxFramesTooltip;"
-                     hidden="true" />
-        <html:button id="command-button-noautohide"
-                     class="command-button command-button-invertable devtools-button"
-                     title="&toolboxNoAutoHideTooltip;"
-                     hidden="true" />
-      </hbox>
-      <vbox id="toolbox-controls-separator" class="devtools-separator"/>
-      <hbox id="toolbox-option-container"/>
-      <hbox id="toolbox-controls">
-        <hbox id="toolbox-dock-buttons"/>
-        <html:button id="toolbox-close"
-                     class="devtools-button"
-                     title="&toolboxCloseButton.tooltip;"/>
-      </hbox>
-    </toolbar>
+    <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-toolbar-mount" />
     <vbox flex="1" class="theme-body">
       <!-- Set large flex to allow the toolbox-panel-webconsole to have a
            height set to a small value without flexing to fill up extra
            space. There must be a flex on both to ensure that the console
            panel itself is sized properly -->
       <box id="toolbox-deck" flex="1000" minheight="75" />
       <splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
       <box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
--- a/devtools/client/inspector/test/browser_inspector_highlighter-preview.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-preview.js
@@ -47,10 +47,10 @@ function* clickElement(selector, testAct
 
 function* checkElementSelected(selector, inspector) {
   let el = yield getNodeFront(selector, inspector);
   is(inspector.selection.nodeFront, el, `The element ${selector} is now selected`);
 }
 
 function checkPickerMode(toolbox, isOn) {
   let pickerButton = toolbox.doc.querySelector("#command-button-pick");
-  is(pickerButton.hasAttribute("checked"), isOn, "The picker mode is correct");
+  is(pickerButton.classList.contains("checked"), isOn, "The picker mode is correct");
 }
--- a/devtools/client/inspector/test/browser_inspector_initialization.js
+++ b/devtools/client/inspector/test/browser_inspector_initialization.js
@@ -52,19 +52,17 @@ function* testToolboxInitialization(test
 
   yield testActor.scrollIntoView("span");
 
   yield selectNode("span", inspector);
   yield testMarkupView("span", inspector);
   yield testBreadcrumbs("span", inspector);
 
   info("Destroying toolbox");
-  let destroyed = toolbox.once("destroyed");
-  toolbox.destroy();
-  yield destroyed;
+  yield toolbox.destroy();
 
   ok("true", "'destroyed' notification received.");
   ok(!gDevTools.getToolbox(target), "Toolbox destroyed.");
 }
 
 function* testContextMenuInitialization(testActor) {
   info("Opening inspector by clicking on 'Inspect Element' context menu item");
   yield clickOnInspectMenuItem(testActor, "#salutation");
--- a/devtools/client/locales/en-US/toolbox.dtd
+++ b/devtools/client/locales/en-US/toolbox.dtd
@@ -5,29 +5,16 @@
 <!-- LOCALIZATION NOTE : FILE This file contains the Toolbox strings -->
 <!-- LOCALIZATION NOTE : FILE Do not translate key -->
 
 <!ENTITY closeCmd.key  "W">
 <!ENTITY toggleToolbox.key  "I">
 <!ENTITY toggleToolboxF12.keycode          "VK_F12">
 <!ENTITY toggleToolboxF12.keytext          "F12">
 
-<!ENTITY toolboxCloseButton.tooltip    "Close Developer Tools">
-
-<!-- LOCALIZATION NOTE (toolboxFramesButton): This is the label for
-  -  the iframes menu list that appears only when the document has some.
-  -  It allows you to switch the context of the whole toolbox. -->
-<!ENTITY toolboxFramesTooltip          "Select an iframe as the currently targeted document">
-
-<!-- LOCALIZATION NOTE (toolboxNoAutoHideButton): This is the label for
-  -  the button to force the popups/panels to stay visible on blur.
-  -  This is only visible in the browser toolbox as it is meant for
-  -  addon developers and Firefox contributors. -->
-<!ENTITY toolboxNoAutoHideTooltip      "Disable popup auto hide">
-
 <!-- LOCALIZATION NOTE (browserToolboxErrorMessage): This is the label
   -  shown next to error details when the Browser Toolbox is unable to open. -->
 <!ENTITY browserToolboxErrorMessage          "Error opening Browser Toolbox:">
 
 <!-- LOCALIZATION NOTE (options.context.advancedSettings): This is the label for
   -  the heading of the advanced settings group in the options panel. -->
 <!ENTITY options.context.advancedSettings "Advanced settings">
 
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -153,8 +153,23 @@ toolbox.forceReload2.key=CmdOrCtrl+F5
 
 # LOCALIZATION NOTE (toolbox.minimize.key)
 # Key shortcut used to minimize the toolbox
 toolbox.minimize.key=CmdOrCtrl+Shift+U
 
 # LOCALIZATION NOTE (toolbox.toggleHost.key)
 # Key shortcut used to move the toolbox in bottom or side of the browser window
 toolbox.toggleHost.key=CmdOrCtrl+Shift+D
+
+# LOCALIZATION NOTE (toolbox.frames.tooltip): This is the label for
+# the iframes menu list that appears only when the document has some.
+# It allows you to switch the context of the whole toolbox.
+toolbox.frames.tooltip=Select an iframe as the currently targeted document
+
+# LOCALIZATION NOTE (toolbox.noautohide.tooltip): This is the label for
+# the button to force the popups/panels to stay visible on blur.
+# This is only visible in the browser toolbox as it is meant for
+# addon developers and Firefox contributors.
+toolbox.noautohide.tooltip=Disable popup auto hide
+
+# LOCALIZATION NOTE (toolbox.closebutton.tooltip): This is the tooltip for
+# the close button the developer tools toolbox.
+toolbox.closebutton.tooltip=Close Developer Tools
--- a/devtools/client/performance/test/browser_perf-highlighted.js
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -16,33 +16,33 @@ add_task(function* () {
   let { target, toolbox, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let tab = toolbox.doc.getElementById("toolbox-tab-performance");
 
   yield console.profile("rust");
-  yield waitUntil(() => tab.hasAttribute("highlighted"));
+  yield waitUntil(() => tab.classList.contains("highlighted"));
 
-  ok(tab.hasAttribute("highlighted"), "Performance tab is highlighted during recording " +
-    "from console.profile when unloaded.");
+  ok(tab.classList.contains("highlighted"), "Performance tab is highlighted during " +
+    "recording from console.profile when unloaded.");
 
   yield console.profileEnd("rust");
-  yield waitUntil(() => !tab.hasAttribute("highlighted"));
+  yield waitUntil(() => !tab.classList.contains("highlighted"));
 
-  ok(!tab.hasAttribute("highlighted"),
+  ok(!tab.classList.contains("highlighted"),
     "Performance tab is no longer highlighted when console.profile recording finishes.");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
 
   yield startRecording(panel);
 
-  ok(tab.hasAttribute("highlighted"),
+  ok(tab.classList.contains("highlighted"),
     "Performance tab is highlighted during recording while in performance tool.");
 
   yield stopRecording(panel);
 
-  ok(!tab.hasAttribute("highlighted"),
+  ok(!tab.classList.contains("highlighted"),
     "Performance tab is no longer highlighted when recording finishes.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -32,16 +32,17 @@ pref("devtools.toolbox.previousHost", "s
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","screenshot --fullpage --file", "rulers", "measure"]');
 pref("devtools.toolbox.sideEnabled", true);
 pref("devtools.toolbox.zoomValue", "1");
 pref("devtools.toolbox.splitconsoleEnabled", false);
 pref("devtools.toolbox.splitconsoleHeight", 100);
 
 // Toolbox Button preferences
+pref("devtools.command-button-pick.enabled", true);
 pref("devtools.command-button-frames.enabled", true);
 pref("devtools.command-button-splitconsole.enabled", true);
 pref("devtools.command-button-paintflashing.enabled", false);
 pref("devtools.command-button-scratchpad.enabled", false);
 pref("devtools.command-button-responsive.enabled", true);
 pref("devtools.command-button-screenshot.enabled", false);
 pref("devtools.command-button-rulers.enabled", false);
 pref("devtools.command-button-measure.enabled", false);
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -5,17 +5,16 @@
 "use strict";
 
 const { Ci } = require("chrome");
 const promise = require("promise");
 const defer = require("devtools/shared/defer");
 const Services = require("Services");
 const { TargetFactory } = require("devtools/client/framework/target");
 const Telemetry = require("devtools/client/shared/telemetry");
-const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
 const {Task} = require("devtools/shared/task");
 
 const NS_XHTML = "http://www.w3.org/1999/xhtml";
 
 const { PluralForm } = require("devtools/shared/plural-form");
 
@@ -62,81 +61,66 @@ var CommandUtils = {
    * @param pref The name of the preference to read
    */
   getCommandbarSpec: function (pref) {
     let value = prefBranch.getComplexValue(pref, Ci.nsISupportsString).data;
     return JSON.parse(value);
   },
 
   /**
-   * A toolbarSpec is an array of strings each of which is a GCLI command.
+   * Create a list of props for React components that manage the state of the buttons.
+   *
+   * @param {Array} toolbarSpec - An array of strings each of which is a GCLI command.
+   * @param {Object} target
+   * @param {Object} document - Used to listen to unload event of the window.
+   * @param {Requisition} requisition
+   * @param {Function} createButtonState - A function that provides a common interface
+   *                                       to create a button for the toolbox.
+   *
+   * @return {Array} List of ToolboxButton objects..
    *
    * Warning: this method uses the unload event of the window that owns the
    * buttons that are of type checkbox. this means that we don't properly
    * unregister event handlers until the window is destroyed.
    */
-  createButtons: function (toolbarSpec, target, document, requisition) {
+  createCommandButtons: function (toolbarSpec, target, document, requisition,
+                                  createButtonState) {
     return util.promiseEach(toolbarSpec, typed => {
       // Ask GCLI to parse the typed string (doesn't execute it)
       return requisition.update(typed).then(() => {
-        let button = document.createElementNS(NS_XHTML, "button");
-
         // Ignore invalid commands
         let command = requisition.commandAssignment.value;
         if (command == null) {
           throw new Error("No command '" + typed + "'");
         }
-
-        if (command.buttonId != null) {
-          button.id = command.buttonId;
-          if (command.buttonClass != null) {
-            button.className = command.buttonClass;
-          }
-        } else {
-          button.setAttribute("text-as-image", "true");
-          button.setAttribute("label", command.name);
+        if (!command.buttonId) {
+          throw new Error("Attempting to add a button to the toolbar, and the command " +
+                          "did not have an id.");
         }
-
-        button.classList.add("devtools-button");
+        // Create the ToolboxButton.
+        let button = createButtonState({
+          id: command.buttonId,
+          className: command.buttonClass,
+          description: command.tooltipText || command.description,
+          onClick: requisition.updateExec.bind(requisition, typed)
+        });
 
-        if (command.tooltipText != null) {
-          button.setAttribute("title", command.tooltipText);
-        } else if (command.description != null) {
-          button.setAttribute("title", command.description);
-        }
-
-        button.addEventListener("click",
-          requisition.updateExec.bind(requisition, typed));
-
-        button.addEventListener("keypress", (event) => {
-          if (ViewHelpers.isSpaceOrReturn(event)) {
-            event.preventDefault();
-            requisition.updateExec(typed);
-          }
-        }, false);
-
-        // Allow the command button to be toggleable
-        let onChange = null;
+        // Allow the command button to be toggleable.
         if (command.state) {
-          button.setAttribute("autocheck", false);
-
           /**
            * The onChange event should be called with an event object that
            * contains a target property which specifies which target the event
            * applies to. For legacy reasons the event object can also contain
            * a tab property.
            */
-          onChange = (eventName, ev) => {
+          const onChange = (eventName, ev) => {
             if (ev.target == target || ev.tab == target.tab) {
               let updateChecked = (checked) => {
-                if (checked) {
-                  button.setAttribute("checked", true);
-                } else if (button.hasAttribute("checked")) {
-                  button.removeAttribute("checked");
-                }
+                // This will emit a ToolboxButton update event.
+                button.isChecked = checked;
               };
 
               // isChecked would normally be synchronous. An annoying quirk
               // of the 'csscoverage toggle' command forces us to accept a
               // promise here, but doing Promise.resolve(reply).then(...) here
               // makes this async for everyone, which breaks some tests so we
               // treat non-promise replies separately to keep then synchronous.
               let reply = command.state.isChecked(target);
@@ -145,24 +129,23 @@ var CommandUtils = {
               } else {
                 updateChecked(reply);
               }
             }
           };
 
           command.state.onChange(target, onChange);
           onChange("", { target: target });
+
+          document.defaultView.addEventListener("unload", function (event) {
+            if (command.state.offChange) {
+              command.state.offChange(target, onChange);
+            }
+          }, { once: true });
         }
-        document.defaultView.addEventListener("unload", function (event) {
-          if (onChange && command.state.offChange) {
-            command.state.offChange(target, onChange);
-          }
-          button.remove();
-          button = null;
-        }, { once: true });
 
         requisition.clear();
 
         return button;
       });
     });
   },
 
--- a/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
+++ b/devtools/client/shared/test/browser_telemetry_button_paintflashing.js
@@ -8,16 +8,17 @@ const TEST_URI = "data:text/html;charset
 
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
 add_task(function* () {
   yield addTab(TEST_URI);
   let Telemetry = loadTelemetryAndRecordLogs();
+  yield pushPref("devtools.command-button-paintflashing.enabled", true);
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let toolbox = yield gDevTools.showToolbox(target, "inspector");
   info("inspector opened");
 
   info("testing the paintflashing button");
   yield testButton(toolbox, Telemetry);
 
--- a/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
+++ b/devtools/client/shared/test/browser_telemetry_button_scratchpad.js
@@ -9,16 +9,18 @@ const TEST_URI = "data:text/html;charset
 // Because we need to gather stats for the period of time that a tool has been
 // opened we make use of setTimeout() to create tool active times.
 const TOOL_DELAY = 200;
 
 add_task(function* () {
   yield addTab(TEST_URI);
   let Telemetry = loadTelemetryAndRecordLogs();
 
+  yield pushPref("devtools.command-button-scratchpad.enabled", true);
+
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let toolbox = yield gDevTools.showToolbox(target, "inspector");
   info("inspector opened");
 
   let onAllWindowsOpened = trackScratchpadWindows();
 
   info("testing the scratchpad button");
   yield testButton(toolbox, Telemetry);
--- a/devtools/client/themes/firebug-theme.css
+++ b/devtools/client/themes/firebug-theme.css
@@ -89,40 +89,56 @@
   -moz-box-flex: initial;
   min-width: 0;
 }
 
 /* Also add negative bottom margin for side panel tabs*/
 .theme-firebug .devtools-sidebar-tabs tab {
 }
 
+.theme-firebug .devtools-tab span {
+  padding-inline-end: 0;
+}
+
+/* Tweak the margin and padding values differently for sidebar and the main tab bar */
+.theme-firebug .devtools-tab,
+.theme-firebug .devtools-tab.selected {
+  padding: 2px 4px 0 4px;
+  margin: 3px 1px -1px;
+}
+
+.theme-firebug .devtools-sidebar-tabs tab {
+  margin: 3px 0 -1px 0;
+  padding: 2px 0 0 0;
+}
+
 /* In order to hide bottom-border of side panel tabs we need
  to make the parent element overflow visible, so child element
  can move one pixel down to hide the bottom border of the parent. */
 .theme-firebug .devtools-sidebar-tabs tabs {
   overflow: visible;
 }
 
 .theme-firebug .devtools-tab:hover,
 .theme-firebug .devtools-sidebar-tabs tab:hover {
   border: 1px solid #C8C8C8 !important;
   border-bottom: 1px solid transparent;
 }
 
-.theme-firebug .devtools-tab[selected],
+.theme-firebug .devtools-tab.selected,
 .theme-firebug .devtools-sidebar-tabs tab[selected] {
   background-color: rgb(247, 251, 254);
   border: 1px solid rgb(170, 188, 207) !important;
   border-bottom-width: 0 !important;
   padding-bottom: 2px;
   color: inherit;
 }
 
-.theme-firebug .devtools-tab spacer,
-.theme-firebug .devtools-tab image {
+.theme-firebug .devtools-tabbar .devtools-separator,
+.theme-firebug .devtools-tab img {
   display: none;
 }
 
 .theme-firebug .toolbox-tab label {
   margin: 0;
 }
 
 .theme-firebug .devtools-sidebar-tabs tab label {
@@ -130,17 +146,17 @@
 }
 
 /* Use different padding for labels inside tabs on Win platform.
   Make sure this overrides the default in global.css */
 :root[platform="win"].theme-firebug .devtools-sidebar-tabs tab label {
   margin: 0 4px !important;
 }
 
-.theme-firebug #panelSideBox .devtools-tab[selected],
+.theme-firebug #panelSideBox .devtools-tab.selected,
 .theme-firebug .devtools-sidebar-tabs tab[selected] {
   background-color: white;
 }
 
 .theme-firebug #panelSideBox .devtools-tab:first-child,
 .theme-firebug .devtools-sidebar-tabs tab:first-child {
   margin-inline-start: 5px;
 }
@@ -150,23 +166,31 @@
 .theme-firebug #toolbox-tab-options {
   margin-inline-end: 4px;
   background-color: white;
 }
 
 .theme-firebug #toolbox-tab-options::before {
   content: url(chrome://devtools/skin/images/firebug/tool-options.svg);
   display: block;
-  margin: 4px 7px 0;
+  margin: 4px 4px 0;
 }
 
 .theme-firebug #toolbox-tab-options:not([selected]):hover::before {
   filter: brightness(80%);
 }
 
+/* Element picker */
+.theme-firebug #toolbox-buttons-start {
+  border: none;
+}
+
+.theme-firebug #command-button-pick {
+    top: 6px;
+}
 /* Toolbar */
 
 .theme-firebug .theme-toolbar,
 .theme-firebug toolbar,
 .theme-firebug .devtools-toolbar {
   border-bottom: 1px solid rgb(170, 188, 207) !important;
   background-color: var(--theme-tab-toolbar-background) !important;
   background-image: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.2));
@@ -228,8 +252,14 @@
 .theme-firebug .devtools-toolbarbutton {
   min-width: 24px;
 }
 
 
 .theme-firebug #element-picker {
   min-height: 21px;
 }
+
+/* Make sure the toolbar buttons shrink nicely. */
+
+#toolbox-buttons-end {
+  background-image: linear-gradient(rgba(253, 253, 253, 0.2), rgba(253, 253, 253, 0));
+}
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -182,17 +182,17 @@
 .devtools-sidebar-tabs tabs > tab[selected]:hover:active {
   color: var(--theme-selection-color);
   background: var(--theme-selection-background);
 }
 
 /* Invert the colors of certain light theme images for displaying
  * inside of the dark theme.
  */
-.devtools-tab[icon-invertable] > image,
+.devtools-tab.icon-invertable > img,
 .devtools-toolbarbutton > image,
 .devtools-button::before,
 #breadcrumb-separator-normal,
 .scrollbutton-up > .toolbarbutton-icon,
 .scrollbutton-down > .toolbarbutton-icon,
 #black-boxed-message-button .button-icon,
 #canvas-debugging-empty-notice-button .button-icon,
 #toggle-breakpoints[checked] > image,
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -44,48 +44,81 @@
 .devtools-tabbar {
   -moz-appearance: none;
   min-height: 24px;
   border: 0px solid;
   border-bottom-width: 1px;
   padding: 0;
   background: var(--theme-tab-toolbar-background);
   border-bottom-color: var(--theme-splitter-color);
+  display: flex;
+}
+
+.toolbox-tabs {
+  margin: 0;
+  flex: 1;
 }
 
-#toolbox-tabs {
-  margin: 0;
+.toolbox-tabs-wrapper {
+  position: relative;
+  display: flex;
+  flex: 1;
+}
+
+.toolbox-tabs {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
 }
 
 /* Set flex attribute to Toolbox buttons and Picker container so,
-   they don't overlapp with the tab bar */
-#toolbox-buttons {
+   they don't overlap with the tab bar */
+#toolbox-buttons-end {
   display: flex;
 }
 
 #toolbox-picker-container {
   display: flex;
 }
 
+#toolbox-buttons-start {
+  border: solid 0 var(--theme-splitter-color);
+  border-inline-end-width: 1px;
+}
+
 /* Toolbox tabs */
-
 .devtools-tab {
-  -moz-appearance: none;
-  -moz-binding: url("chrome://global/content/bindings/general.xml#control-item");
-  -moz-box-align: center;
   min-width: 32px;
   min-height: 24px;
-  max-width: 100px;
   margin: 0;
   padding: 0;
   border-style: solid;
   border-width: 0;
   border-inline-start-width: 1px;
-  -moz-box-align: center;
-  -moz-box-flex: 1;
+  padding-inline-end: 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  background-color: transparent;
+}
+
+.devtools-tab-icon-only {
+  padding-inline-end: 2px;
+}
+
+.devtools-tab span {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  padding-inline-end: 13px;
+  position: relative;
+  top: 1px;
 }
 
 /* Save space on the tab-strip in Firebug theme */
 .theme-firebug .devtools-tab {
   -moz-box-flex: initial;
 }
 
 .theme-dark .devtools-tab {
@@ -109,125 +142,126 @@
 .theme-dark .devtools-tab:hover:active {
   color: var(--theme-selection-color);
 }
 
 .devtools-tab:hover:active {
   background-color: var(--toolbar-tab-hover-active);
 }
 
-.theme-dark .devtools-tab:not([selected])[highlighted] {
+.theme-dark .devtools-tab:not(.selected).highlighted {
   background-color: hsla(99, 100%, 14%, .3);
 }
 
-.theme-light .devtools-tab:not([selected])[highlighted] {
+.theme-light .devtools-tab:not(.selected).highlighted {
   background-color: rgba(44, 187, 15, .2);
 }
 
 /* Display execution pointer in the Debugger tab to indicate
    that the debugger is paused. */
-.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not([selected])[highlighted] {
+.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not(.selected).highlighted {
   background-color: rgba(89, 178, 234, .2);
   background-image: url(chrome://devtools/skin/images/firebug/tool-debugger-paused.svg);
   background-repeat: no-repeat;
   padding-left: 13px !important;
   background-position: 3px 6px;
 }
 
-.devtools-tab > image {
+.devtools-tab > img {
   border: none;
   margin: 0;
-  margin-inline-start: 4px;
+  margin-inline-start: 10px;
   opacity: 0.6;
   max-height: 16px;
   width: 16px; /* Prevents collapse during theme switching */
+  vertical-align: text-top;
+  margin-inline-end: 6px;
 }
 
 /* Support invertable icon flags and make icon white when it's on a blue background */
-.theme-light .devtools-tab[icon-invertable="light-theme"]:not([selected]) > image,
-.devtools-tab[icon-invertable="dark-theme"][selected] > image {
+.theme-light .devtools-tab.icon-invertable-light-theme:not(.selected) > img,
+.devtools-tab.icon-invertable-dark-theme.selected > img {
   filter: invert(1);
 }
 
 /* Don't apply any filter to non-invertable command button icons */
 .command-button:not(.command-button-invertable),
-/* [icon-invertable="light-theme"] icons are white, so do not invert them for the dark theme */
-.theme-dark .devtools-tab[icon-invertable="light-theme"] > image,
+/* icon-invertable-light-theme icons are white, so do not invert them for the dark theme */
+.theme-dark .devtools-tab.icon-invertable-light-theme > img,
 /* Since "highlighted" icons are green, we should omit the filter */
-.devtools-tab[icon-invertable][highlighted]:not([selected]) > image {
+.devtools-tab.icon-invertable.highlighted:not(.selected) > img {
   filter: none;
 }
 
 .devtools-tab > label {
   white-space: nowrap;
   margin: 0 4px;
 }
 
-.devtools-tab:hover > image {
+.devtools-tab:hover > img {
   opacity: 0.8;
 }
 
-.devtools-tab:active > image,
-.devtools-tab[selected] > image {
+.devtools-tab:active > img,
+.devtools-tab.selected > img {
   opacity: 1;
 }
 
-.devtools-tabbar .devtools-tab[selected],
-.devtools-tabbar .devtools-tab[selected]:hover:active {
+.devtools-tabbar .devtools-tab.selected,
+.devtools-tabbar .devtools-tab.selected:hover:active {
   color: var(--theme-selection-color);
   background-color: var(--theme-selection-background);
 }
 
-#toolbox-tabs .devtools-tab[selected],
-#toolbox-tabs .devtools-tab[highlighted] {
+.toolbox-tabs .devtools-tab.selected,
+.toolbox-tabs .devtools-tab.highlighted {
   border-width: 0;
   padding-inline-start: 1px;
 }
 
-#toolbox-tabs .devtools-tab[selected]:last-child,
-#toolbox-tabs .devtools-tab[highlighted]:last-child {
-  padding-inline-end: 1px;
-}
-
-#toolbox-tabs .devtools-tab[selected] + .devtools-tab,
-#toolbox-tabs .devtools-tab[highlighted] + .devtools-tab {
+.toolbox-tabs .devtools-tab.selected + .devtools-tab,
+.toolbox-tabs .devtools-tab.highlighted + .devtools-tab {
   border-inline-start-width: 0;
   padding-inline-start: 1px;
 }
 
-#toolbox-tabs .devtools-tab:first-child[selected] {
+.toolbox-tabs .devtools-tab:first-child {
   border-inline-start-width: 0;
 }
 
-#toolbox-tabs .devtools-tab:last-child {
+.toolbox-tabs .devtools-tab:last-child {
   border-inline-end-width: 1px;
 }
 
-.devtools-tab:not([highlighted]) > .highlighted-icon,
-.devtools-tab[selected] > .highlighted-icon,
-.devtools-tab:not([selected])[highlighted] > .default-icon {
-  visibility: collapse;
+.devtools-tab:not(.highlighted) > .highlighted-icon,
+.devtools-tab.selected > .highlighted-icon,
+.devtools-tab:not(.selected).highlighted > .default-icon {
+  display: none;
 }
 
 /* The options tab is special - it doesn't have the same parent
    as the other tabs (toolbox-option-container vs toolbox-tabs) */
-#toolbox-option-container .devtools-tab:not([selected]) {
+#toolbox-option-container .devtools-tab:not(.selected) {
   background-color: transparent;
 }
 #toolbox-option-container .devtools-tab {
   border-color: transparent;
   border-width: 0;
   padding-inline-start: 1px;
 }
-#toolbox-tab-options > image {
-  margin: 0 8px;
+#toolbox-option-container img {
+  margin-inline-end: 6px;
+  margin-inline-start: 6px;
 }
 
 /* Toolbox controls */
 
+#toolbox-controls, #toolbox-dock-buttons {
+  display: flex;
+}
 #toolbox-controls > button,
 #toolbox-dock-buttons > button {
   -moz-appearance: none;
   border: none;
   margin: 0 4px;
   min-width: 16px;
   width: 16px;
 }
@@ -258,17 +292,26 @@
 #toolbox-dock-bottom-minimize::before {
   background-image: url("chrome://devtools/skin/images/dock-bottom-minimize@2x.png");
 }
 
 #toolbox-dock-bottom-minimize.minimized::before {
   background-image: url("chrome://devtools/skin/images/dock-bottom-maximize@2x.png");
 }
 
-#toolbox-buttons:empty + .devtools-separator,
+/**
+ * Ensure that when the toolbar collapses in on itself when there is not enough room
+ * that it still looks reasonable.
+ */
+.devtools-tabbar > div {
+  background-color: var(--theme-tab-toolbar-background);
+  z-index: 0;
+}
+
+#toolbox-buttons-end:empty + .devtools-separator,
 .devtools-separator[invisible] {
   visibility: hidden;
 }
 
 #toolbox-controls-separator {
   margin: 0;
 }
 
@@ -288,32 +331,32 @@
   background-color: var(--toolbar-tab-hover);
 }
 
 .theme-light .command-button:hover {
   background-color: inherit;
 }
 
 .command-button:hover:active,
-.command-button[checked=true]:not(:hover) {
+.command-button.checked:not(:hover) {
   background-color: var(--toolbar-tab-hover-active)
 }
 
 .theme-light .command-button:hover:active,
-.theme-light .command-button[checked=true]:not(:hover) {
+.theme-light .command-button.checked:not(:hover) {
   background-color: inherit;
 }
 
 .command-button:hover::before {
   opacity: 0.85;
 }
 
 .command-button:hover:active::before,
-.command-button[checked=true]::before,
-.command-button[open=true]::before {
+.command-button.checked::before,
+.command-button.open::before {
   opacity: 1;
 }
 
 /* Command button images */
 
 #command-button-paintflashing::before {
   background-image: var(--command-paintflashing-image);
 }
--- a/devtools/client/webconsole/test/browser_webconsole_split.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split.js
@@ -80,152 +80,143 @@ function runTest() {
     let cmdButton = toolbox.doc.querySelector("#command-button-splitconsole");
 
     return {
       deckHeight: deckHeight,
       containerHeight: containerHeight,
       webconsoleHeight: webconsoleHeight,
       splitterVisibility: splitterVisibility,
       openedConsolePanel: openedConsolePanel,
-      buttonSelected: cmdButton.hasAttribute("checked")
+      buttonSelected: cmdButton.classList.contains("checked")
     };
   }
 
-  function checkWebconsolePanelOpened() {
+  const checkWebconsolePanelOpened = Task.async(function* () {
     info("About to check special cases when webconsole panel is open.");
 
-    let deferred = promise.defer();
-
     // Start with console split, so we can test for transition to main panel.
-    toolbox.toggleSplitConsole();
+    yield toolbox.toggleSplitConsole();
 
     let currentUIState = getCurrentUIState();
 
     ok(currentUIState.splitterVisibility,
        "Splitter is visible when console is split");
     ok(currentUIState.deckHeight > 0,
        "Deck has a height > 0 when console is split");
     ok(currentUIState.webconsoleHeight > 0,
        "Web console has a height > 0 when console is split");
     ok(!currentUIState.openedConsolePanel,
        "The console panel is not the current tool");
     ok(currentUIState.buttonSelected, "The command button is selected");
 
-    openPanel("webconsole").then(() => {
-      currentUIState = getCurrentUIState();
-
-      ok(!currentUIState.splitterVisibility,
-         "Splitter is hidden when console is opened.");
-      is(currentUIState.deckHeight, 0,
-         "Deck has a height == 0 when console is opened.");
-      is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
-         "Web console is full height.");
-      ok(currentUIState.openedConsolePanel,
-         "The console panel is the current tool");
-      ok(currentUIState.buttonSelected,
-         "The command button is still selected.");
+    yield openPanel("webconsole");
+    currentUIState = getCurrentUIState();
 
-      // Make sure splitting console does nothing while webconsole is opened
-      toolbox.toggleSplitConsole();
-
-      currentUIState = getCurrentUIState();
+    ok(!currentUIState.splitterVisibility,
+       "Splitter is hidden when console is opened.");
+    is(currentUIState.deckHeight, 0,
+       "Deck has a height == 0 when console is opened.");
+    is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
+       "Web console is full height.");
+    ok(currentUIState.openedConsolePanel,
+       "The console panel is the current tool");
+    ok(currentUIState.buttonSelected,
+       "The command button is still selected.");
 
-      ok(!currentUIState.splitterVisibility,
-         "Splitter is hidden when console is opened.");
-      is(currentUIState.deckHeight, 0,
-         "Deck has a height == 0 when console is opened.");
-      is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
-         "Web console is full height.");
-      ok(currentUIState.openedConsolePanel,
-         "The console panel is the current tool");
-      ok(currentUIState.buttonSelected,
-         "The command button is still selected.");
+    // Make sure splitting console does nothing while webconsole is opened
+    yield toolbox.toggleSplitConsole();
+
+    currentUIState = getCurrentUIState();
 
-      // Make sure that split state is saved after opening another panel
-      openPanel("inspector").then(() => {
-        currentUIState = getCurrentUIState();
-        ok(currentUIState.splitterVisibility,
-           "Splitter is visible when console is split");
-        ok(currentUIState.deckHeight > 0,
-           "Deck has a height > 0 when console is split");
-        ok(currentUIState.webconsoleHeight > 0,
-           "Web console has a height > 0 when console is split");
-        ok(!currentUIState.openedConsolePanel,
-           "The console panel is not the current tool");
-        ok(currentUIState.buttonSelected,
-           "The command button is still selected.");
-
-        toolbox.toggleSplitConsole();
-        deferred.resolve();
-      });
-    });
-    return deferred.promise;
-  }
+    ok(!currentUIState.splitterVisibility,
+       "Splitter is hidden when console is opened.");
+    is(currentUIState.deckHeight, 0,
+       "Deck has a height == 0 when console is opened.");
+    is(currentUIState.webconsoleHeight, currentUIState.containerHeight,
+       "Web console is full height.");
+    ok(currentUIState.openedConsolePanel,
+       "The console panel is the current tool");
+    ok(currentUIState.buttonSelected,
+       "The command button is still selected.");
 
-  function openPanel(toolId) {
-    let deferred = promise.defer();
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    gDevTools.showToolbox(target, toolId).then(function (box) {
-      toolbox = box;
-      deferred.resolve();
-    }).then(null, console.error);
-    return deferred.promise;
-  }
+    // Make sure that split state is saved after opening another panel
+    yield openPanel("inspector");
+    currentUIState = getCurrentUIState();
+    ok(currentUIState.splitterVisibility,
+       "Splitter is visible when console is split");
+    ok(currentUIState.deckHeight > 0,
+       "Deck has a height > 0 when console is split");
+    ok(currentUIState.webconsoleHeight > 0,
+       "Web console has a height > 0 when console is split");
+    ok(!currentUIState.openedConsolePanel,
+       "The console panel is not the current tool");
+    ok(currentUIState.buttonSelected,
+       "The command button is still selected.");
 
-  function openAndCheckPanel(toolId) {
-    let deferred = promise.defer();
-    openPanel(toolId).then(() => {
-      info("Checking toolbox for " + toolId);
-      checkToolboxUI(toolbox.getCurrentPanel());
-      deferred.resolve();
-    });
-    return deferred.promise;
-  }
+    yield toolbox.toggleSplitConsole();
+  });
 
-  function checkToolboxUI() {
+  const checkToolboxUI = Task.async(function* () {
     let currentUIState = getCurrentUIState();
 
     ok(!currentUIState.splitterVisibility, "Splitter is hidden by default");
     is(currentUIState.deckHeight, currentUIState.containerHeight,
        "Deck has a height > 0 by default");
     is(currentUIState.webconsoleHeight, 0,
        "Web console is collapsed by default");
     ok(!currentUIState.openedConsolePanel,
        "The console panel is not the current tool");
     ok(!currentUIState.buttonSelected, "The command button is not selected.");
 
-    toolbox.toggleSplitConsole();
+    yield toolbox.toggleSplitConsole();
 
     currentUIState = getCurrentUIState();
 
     ok(currentUIState.splitterVisibility,
        "Splitter is visible when console is split");
     ok(currentUIState.deckHeight > 0,
        "Deck has a height > 0 when console is split");
     ok(currentUIState.webconsoleHeight > 0,
        "Web console has a height > 0 when console is split");
     is(Math.round(currentUIState.deckHeight + currentUIState.webconsoleHeight),
        currentUIState.containerHeight,
        "Everything adds up to container height");
     ok(!currentUIState.openedConsolePanel,
        "The console panel is not the current tool");
     ok(currentUIState.buttonSelected, "The command button is selected.");
 
-    toolbox.toggleSplitConsole();
+    yield toolbox.toggleSplitConsole();
 
     currentUIState = getCurrentUIState();
 
     ok(!currentUIState.splitterVisibility, "Splitter is hidden after toggling");
     is(currentUIState.deckHeight, currentUIState.containerHeight,
        "Deck has a height > 0 after toggling");
     is(currentUIState.webconsoleHeight, 0,
        "Web console is collapsed after toggling");
     ok(!currentUIState.openedConsolePanel,
        "The console panel is not the current tool");
     ok(!currentUIState.buttonSelected, "The command button is not selected.");
+  });
+
+  function openPanel(toolId) {
+    let deferred = promise.defer();
+    let target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.showToolbox(target, toolId).then(function (box) {
+      toolbox = box;
+      deferred.resolve();
+    }).then(null, console.error);
+    return deferred.promise;
+  }
+
+  function openAndCheckPanel(toolId) {
+    return openPanel(toolId).then(() => {
+      info("Checking toolbox for " + toolId);
+      return checkToolboxUI(toolbox.getCurrentPanel());
+    });
   }
 
   function testBottomHost() {
     checkHostType(Toolbox.HostType.BOTTOM);
 
     checkToolboxUI();
 
     toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
--- a/devtools/client/webconsole/test/browser_webconsole_split_persist.js
+++ b/devtools/client/webconsole/test/browser_webconsole_split_persist.js
@@ -94,17 +94,17 @@
   }
 
   function getHeightPrefValue() {
     return Services.prefs.getIntPref("devtools.toolbox.splitconsoleHeight");
   }
 
   function isCommandButtonChecked() {
     return toolbox.doc.querySelector("#command-button-splitconsole")
-      .hasAttribute("checked");
+      .classList.contains("checked");
   }
 
   function toggleSplitConsoleWithEscape() {
     let onceSplitConsole = toolbox.once("split-console");
     let contentWindow = toolbox.win;
     contentWindow.focus();
     EventUtils.sendKey("ESCAPE", contentWindow);
     return onceSplitConsole;