Backed out 3 changesets (bug 1555060) for causing test_tabbar.py to perma fail CLOSED TREE
authorCiure Andrei <aciure@mozilla.com>
Fri, 28 Jun 2019 00:21:50 +0300
changeset 543484 0be464c5cc7deada9ff92db2966fc929a1bcb344
parent 543483 a5c6deeda8a9475ac0268a4351417c8ff659c962
child 543485 d0fb55daae2858b898f1a839a428340c3f149b28
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1555060
milestone69.0a1
backs outa5c6deeda8a9475ac0268a4351417c8ff659c962
f4e21e465f384b90fa1e768141c4db708748bf66
c71c45fe3e63c9fb97551654d079344448c49c6e
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 3 changesets (bug 1555060) for causing test_tabbar.py to perma fail CLOSED TREE Backed out changeset a5c6deeda8a9 (bug 1555060) Backed out changeset f4e21e465f38 (bug 1555060) Backed out changeset c71c45fe3e63 (bug 1555060)
accessible/tests/mochitest/relations/test_tabbrowser.xul
accessible/xul/XULTabAccessible.cpp
browser/base/content/browser.css
browser/base/content/browser.xhtml
browser/base/content/tabbrowser-tab.js
browser/base/content/tabbrowser-tabs.js
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/general/browser_bug462673.js
browser/base/content/test/general/browser_ctrlTab.js
browser/base/content/test/general/browser_tabs_owner.js
browser/base/content/test/performance/browser_startup_images.js
browser/base/content/test/performance/browser_tabclose.js
browser/base/content/test/performance/browser_tabdetach.js
browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
browser/base/content/test/performance/head.js
browser/base/content/test/tabs/browser_audioTabIcon.js
browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js
browser/base/content/test/tabs/browser_overflowScroll.js
browser/base/content/test/tabs/browser_tabReorder_overflow.js
browser/base/jar.mn
browser/components/contextualidentity/test/browser/browser_newtabButton.js
browser/components/contextualidentity/test/browser/browser_windowName.js
browser/components/customizableui/test/browser_newtab_button_customizemode.js
browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
browser/components/places/tests/browser/browser_sidebarpanels_click.js
browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
browser/modules/test/browser/browser_taskbar_preview.js
browser/themes/shared/tabs.inc.css
dom/push/PushRecord.jsm
testing/mochitest/browser-test.js
toolkit/components/contextualidentity/ContextualIdentityService.jsm
toolkit/content/jar.mn
toolkit/content/tests/chrome/test_tabbox.xul
toolkit/content/widgets/tabbox.js
toolkit/content/widgets/tabbox.xml
toolkit/content/xul.css
--- a/accessible/tests/mochitest/relations/test_tabbrowser.xul
+++ b/accessible/tests/mochitest/relations/test_tabbrowser.xul
@@ -41,17 +41,17 @@
         });
       }
 
       this.finalCheck = function testTabRelations_finalCheck(aEvent)
       {
         ////////////////////////////////////////////////////////////////////////
         // 'labelled by'/'label for' relations for xul:tab and xul:tabpanel
 
-        var tabs = Array.from(tabBrowser().tabContainer.allTabs);
+        var tabs = Array.from(tabBrowser().tabContainer.children);
         // For preloaded tabs, there might be items in this array where this relation
         // doesn't hold, so just deal with that:
         var panels = tabs.map(t => t.linkedBrowser.closest("tabpanels > *"));
 
         testRelation(panels[0], RELATION_LABELLED_BY, tabs[0]);
         testRelation(tabs[0], RELATION_LABEL_FOR, panels[0]);
         testRelation(panels[1], RELATION_LABELLED_BY, tabs[1]);
         testRelation(tabs[1], RELATION_LABEL_FOR, panels[1]);
--- a/accessible/xul/XULTabAccessible.cpp
+++ b/accessible/xul/XULTabAccessible.cpp
@@ -82,19 +82,17 @@ uint64_t XULTabAccessible::NativeInterac
   return (state & states::UNAVAILABLE) ? state : state | states::SELECTABLE;
 }
 
 Relation XULTabAccessible::RelationByType(RelationType aType) const {
   Relation rel = AccessibleWrap::RelationByType(aType);
   if (aType != RelationType::LABEL_FOR) return rel;
 
   // Expose 'LABEL_FOR' relation on tab accessible for tabpanel accessible.
-  ErrorResult rv;
-  nsIContent* parent =
-      mContent->AsElement()->Closest(NS_LITERAL_STRING("tabs"), rv);
+  nsIContent* parent = mContent->GetParent();
   if (!parent) return rel;
 
   nsCOMPtr<nsIDOMXULRelatedElement> tabsElm =
       parent->AsElement()->AsXULRelated();
   if (!tabsElm) return rel;
 
   RefPtr<mozilla::dom::Element> tabpanelElement;
   tabsElm->GetRelatedElement(GetNode(), getter_AddRefs(tabpanelElement));
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -121,16 +121,20 @@ panelview[mainview] > .panel-header {
   position: absolute;
 }
 
 .panel-viewstack {
   overflow: visible;
   transition: height var(--panelui-subview-transition-duration);
 }
 
+#tabbrowser-tabs {
+  -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs");
+}
+
 @supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") {
   #tabbrowser-tabs {
     /* Without this, the tabs container width extends beyond the window width */
     width: 0;
   }
   .tab-stack {
     /* Without this, pinned tabs get a bit too tall when the tabstrip overflows. */
     vertical-align: top;
@@ -194,23 +198,23 @@ panelview[mainview] > .panel-header {
 %ifdef NIGHTLY_BUILD
 @supports -moz-bool-pref("browser.tabs.hideThrobber") {
   .tab-throbber {
     display: none !important;
   }
 }
 %endif
 
-#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] {
+#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
   position: fixed !important;
   display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
 }
 
-#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[selected],
-#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[multiselected] {
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected],
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[multiselected] {
   position: relative;
   z-index: 2;
   pointer-events: none; /* avoid blocking dragover events on scroll buttons */
 }
 
 .tabbrowser-tab[tab-grouping],
 .tabbrowser-tab[tabdrop-samewindow],
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]):not([multiselected]) {
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -90,17 +90,16 @@
   }
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-media.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-pageActions.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-plugins.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-sidebar.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/browser-tabsintitlebar.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser-tab.js", this);
-  Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser-tabs.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
   Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
 
   window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
   window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
   window.onclose = WindowIsClosing;
 
   window.addEventListener("MozBeforeInitialXULLayout",
@@ -719,29 +718,21 @@
                context="toolbar-context-menu"
                flex="1">
 
         <hbox class="titlebar-spacer" type="pre-tabs"/>
 
         <hbox flex="1" align="end" class="toolbar-items">
           <hbox id="TabsToolbar-customization-target" flex="1">
             <tabs id="tabbrowser-tabs"
-                  is="tabbrowser-tabs"
                   flex="1"
                   setfocus="false"
                   tooltip="tabbrowser-tab-tooltip"
                   stopwatchid="FX_TAB_CLICK_MS">
-              <hbox class="tab-drop-indicator-box">
-                <image class="tab-drop-indicator" hidden="true"/>
-              </hbox>
-              <arrowscrollbox orient="horizontal" flex="1" style="min-width: 1px;" clicktoscroll="true" class="tabbrowser-arrowscrollbox" scrolledtostart="true" scrolledtoend="true">
-                <tab is="tabbrowser-tab" class="tabbrowser-tab" selected="true" visuallyselected="true" fadein="true"/>
-                <toolbarbutton class="tabs-newtab-button toolbarbutton-1" command="cmd_newNavigatorTab" onclick="checkForMiddleClick(this, event);"/>
-                <spacer class="closing-tabs-spacer" style="width: 0;"/>
-              </arrowscrollbox>
+              <tab is="tabbrowser-tab" class="tabbrowser-tab" selected="true" visuallyselected="true" fadein="true"/>
             </tabs>
 
             <toolbarbutton id="new-tab-button"
                            class="toolbarbutton-1 chromeclass-toolbar-additional"
                            label="&tabCmd.label;"
                            command="cmd_newNavigatorTab"
                            onclick="checkForMiddleClick(this, event);"
                            tooltip="dynamic-shortcut-tooltip"
--- a/browser/base/content/tabbrowser-tab.js
+++ b/browser/base/content/tabbrowser-tab.js
@@ -100,20 +100,16 @@ class MozTabbrowserTab extends MozElemen
     this.setAttribute("context", "tabContextMenu");
     this._initialized = true;
 
     if (!("_lastAccessed" in this)) {
       this.updateLastAccessed();
     }
   }
 
-  get container() {
-    return gBrowser.tabContainer;
-  }
-
   set _visuallySelected(val) {
     if (val == (this.getAttribute("visuallyselected") == "true")) {
       return val;
     }
 
     if (val) {
       this.setAttribute("visuallyselected", "true");
     } else {
@@ -270,17 +266,17 @@ class MozTabbrowserTab extends MozElemen
       this.style.MozUserFocus = "";
     } else if (this.mOverCloseButton) {
       event.stopPropagation();
     }
   }
 
   on_mousedown(event) {
     let eventMaySelectTab = true;
-    let tabContainer = this.container;
+    let tabContainer = this.parentNode;
 
     if (tabContainer._closeTabByDblclick &&
         event.button == 0 &&
         event.detail == 1) {
       this._selectedOnFirstMouseDown = this.selected;
     }
 
     if (this.selected) {
@@ -391,17 +387,17 @@ class MozTabbrowserTab extends MozElemen
       return;
     }
 
     // for the one-close-button case
     if (event.target.classList.contains("tab-close-button")) {
       event.stopPropagation();
     }
 
-    let tabContainer = this.container;
+    let tabContainer = this.parentNode;
     if (tabContainer._closeTabByDblclick &&
         this._selectedOnFirstMouseDown &&
         this.selected &&
         !(event.target.classList.contains("tab-icon-sound") ||
           event.target.classList.contains("tab-icon-overlay"))) {
       gBrowser.removeTab(this, {
         animate: true,
         byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
@@ -415,17 +411,17 @@ class MozTabbrowserTab extends MozElemen
     }
   }
 
   _mouseenter() {
     if (this.hidden || this.closing) {
       return;
     }
 
-    let tabContainer = this.container;
+    let tabContainer = this.parentNode;
     let visibleTabs = tabContainer._getVisibleTabs();
     let tabIndex = visibleTabs.indexOf(this);
 
     if (this.selected) {
       tabContainer._handleTabSelect();
     }
 
     if (tabIndex == 0) {
@@ -463,17 +459,17 @@ class MozTabbrowserTab extends MozElemen
     let tabToWarm = this;
     if (this.mOverCloseButton) {
       tabToWarm = gBrowser._findTabToBlurTo(this);
     }
     gBrowser.warmupTab(tabToWarm);
   }
 
   _mouseleave() {
-    let tabContainer = this.container;
+    let tabContainer = this.parentNode;
     if (tabContainer._beforeHoveredTab) {
       tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
       tabContainer._beforeHoveredTab = null;
     }
     if (tabContainer._afterHoveredTab) {
       tabContainer._afterHoveredTab.removeAttribute("afterhovered");
       tabContainer._afterHoveredTab = null;
     }
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -1,14 +1,14 @@
 /* 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/. */
 
 .tab-close-button[pinned],
-#tabbrowser-tabs[closebuttons="activetab"] > .tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > .tab-close-button:not([selected="true"]),
+#tabbrowser-tabs[closebuttons="activetab"] > .tabbrowser-tab > .tab-stack > .tab-content > .tab-close-button:not([selected="true"]),
 .tab-icon-pending:not([pendingicon]),
 .tab-icon-pending[busy],
 .tab-icon-pending[pinned],
 .tab-icon-image:not([src]):not([pinned]):not([crashed])[selected],
 .tab-icon-image:not([src]):not([pinned]):not([crashed]):not([sharing]),
 .tab-icon-image[busy],
 .tab-throbber:not([busy]),
 .tab-icon-sound:not([soundplaying]):not([muted]):not([activemedia-blocked]):not([pictureinpicture]),
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -27,17 +27,16 @@ window._gBrowser = {
     Services.els.addSystemEventListener(document, "keydown", this, false);
     if (AppConstants.platform == "macosx") {
       Services.els.addSystemEventListener(document, "keypress", this, false);
     }
     window.addEventListener("sizemodechange", this);
     window.addEventListener("occlusionstatechange", this);
     window.addEventListener("framefocusrequested", this);
 
-    this.tabContainer.init();
     this._setupInitialBrowserAndTab();
 
     if (Services.prefs.getBoolPref("browser.display.use_system_colors")) {
       this.tabpanels.style.backgroundColor = "-moz-default-background-color";
     } else if (Services.prefs.getIntPref("browser.display.document_color_use") == 2) {
       this.tabpanels.style.backgroundColor =
         Services.prefs.getCharPref("browser.display.background_color");
     }
@@ -63,29 +62,26 @@ window._gBrowser = {
     XPCOMUtils.defineLazyModuleGetters(this, {
       E10SUtils: "resource://gre/modules/E10SUtils.jsm",
     });
 
     XPCOMUtils.defineLazyPreferenceGetter(this, "animationsEnabled",
       "toolkit.cosmeticAnimations.enabled");
 
     this._setupEventListeners();
-    this._initialized = true;
   },
 
   ownerGlobal: window,
 
   ownerDocument: document,
 
   closingTabsEnum: { ALL: 0, OTHER: 1, TO_END: 2, MULTI_SELECTED: 3 },
 
   _visibleTabs: null,
 
-  _tabs: null,
-
   _lastRelatedTabMap: new WeakMap(),
 
   mProgressListeners: [],
 
   mTabsProgressListeners: [],
 
   _tabListeners: new Map(),
 
@@ -204,42 +200,43 @@ window._gBrowser = {
   _hoverTabTimer: null,
 
   get tabContainer() {
     delete this.tabContainer;
     return this.tabContainer = document.getElementById("tabbrowser-tabs");
   },
 
   get tabs() {
-    if (!this._tabs) {
-      this._tabs = this.tabContainer.allTabs;
-    }
-    return this._tabs;
+    delete this.tabs;
+    return this.tabs = this.tabContainer.children;
   },
 
   get tabbox() {
     delete this.tabbox;
     return this.tabbox = document.getElementById("tabbrowser-tabbox");
   },
 
   get tabpanels() {
     delete this.tabpanels;
     return this.tabpanels = document.getElementById("tabbrowser-tabpanels");
   },
 
-  addEventListener(...args) {
-    this.tabpanels.addEventListener(...args);
-  },
-
-  removeEventListener(...args) {
-    this.tabpanels.removeEventListener(...args);
-  },
-
-  dispatchEvent(...args) {
-    return this.tabpanels.dispatchEvent(...args);
+  get addEventListener() {
+    delete this.addEventListener;
+    return this.addEventListener = this.tabpanels.addEventListener.bind(this.tabpanels);
+  },
+
+  get removeEventListener() {
+    delete this.removeEventListener;
+    return this.removeEventListener = this.tabpanels.removeEventListener.bind(this.tabpanels);
+  },
+
+  get dispatchEvent() {
+    delete this.dispatchEvent;
+    return this.dispatchEvent = this.tabpanels.dispatchEvent.bind(this.tabpanels);
   },
 
   get visibleTabs() {
     if (!this._visibleTabs) {
       this._visibleTabs =
         Array.prototype.filter.call(this.tabs, tab => !tab.hidden && !tab.closing);
     }
     return this._visibleTabs;
@@ -493,21 +490,16 @@ window._gBrowser = {
   set userTypedValue(val) {
     this.selectedBrowser.userTypedValue = val;
   },
 
   get userTypedValue() {
     return this.selectedBrowser.userTypedValue;
   },
 
-  _invalidateCachedTabs() {
-    this._tabs = null;
-    this._visibleTabs = null;
-  },
-
   _setFindbarData() {
     // Ensure we know what the find bar key is in the content process:
     let {sharedData} = Services.ppmm;
     if (!sharedData.has("Findbar:Shortcut")) {
       let keyEl = document.getElementById("key_find");
       let mods = keyEl.getAttribute("modifiers")
         .replace(/accel/i, AppConstants.platform == "macosx" ? "meta" : "control");
       sharedData.set("Findbar:Shortcut", {
@@ -637,22 +629,22 @@ window._gBrowser = {
     } finally {
       this.selectedTab = currentTab;
       this._previewMode = false;
     }
   },
 
   syncThrobberAnimations(aTab) {
     aTab.ownerGlobal.promiseDocumentFlushed(() => {
-      if (!aTab.container) {
+      if (!aTab.parentNode) {
         return;
       }
 
       const animations =
-        Array.from(aTab.container.getElementsByTagName("tab"))
+        Array.from(aTab.parentNode.getElementsByTagName("tab"))
         .map(tab => {
           const throbber = tab.throbber;
           return throbber ? throbber.getAnimations({ subtree: true }) : [];
         })
         .reduce((a, b) => a.concat(b))
         .filter(anim =>
           anim instanceof CSSAnimation &&
           (anim.animationName === "tab-throbber-animation" ||
@@ -2370,16 +2362,19 @@ window._gBrowser = {
 
       // Call _handleNewTab asynchronously as it needs to know if the
       // new tab is selected.
       setTimeout(function(tabContainer) {
         tabContainer._handleNewTab(t);
       }, 0, this.tabContainer);
     }
 
+    // invalidate cache
+    this._visibleTabs = null;
+
     let usingPreloadedContent = false;
     let b;
 
     try {
       // If this new tab is owned by another, assert that relationship
       if (ownerTab) {
         t.owner = ownerTab;
       }
@@ -2416,18 +2411,19 @@ window._gBrowser = {
       if (pinned) {
         index = Math.max(index, 0);
         index = Math.min(index, this._numPinnedTabs);
       } else {
         index = Math.max(index, this._numPinnedTabs);
         index = Math.min(index, this.tabs.length);
       }
 
-      let tabAfter = this.tabs[index] || null;
-      this._invalidateCachedTabs();
+      // Use .item() instead of [] because we need .item() to return null in
+      // order to append the tab at the end in case index == tabs.length.
+      let tabAfter = this.tabs.item(index);
       this.tabContainer.insertBefore(t, tabAfter);
       if (tabAfter) {
         this._updateTabsAfterInsert();
       } else {
         t._tPos = index;
       }
 
       if (pinned) {
@@ -2841,17 +2837,17 @@ window._gBrowser = {
     // We're animating, so we can cancel the non-animation stopwatch.
     TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
 
     aTab.style.maxWidth = ""; // ensure that fade-out transition happens
     aTab.removeAttribute("fadein");
     aTab.removeAttribute("bursting");
 
     setTimeout(function(tab, tabbrowser) {
-      if (tab.container &&
+      if (tab.parentNode &&
           window.getComputedStyle(tab).maxWidth == "0.1px") {
         console.assert(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
         tabbrowser._endRemoveTab(tab);
       }
     }, 3000, aTab, this);
   },
 
   _hasBeforeUnload(aTab) {
@@ -2961,17 +2957,17 @@ window._gBrowser = {
     if (!adoptedByTab && aTab.hasAttribute("soundplaying")) {
       // Don't persist the muted state as this wasn't a user action.
       // This lets undo-close-tab return it to an unmuted state.
       aTab.linkedBrowser.mute(true);
     }
 
     aTab.closing = true;
     this._removingTabs.push(aTab);
-    this._invalidateCachedTabs();
+    this._visibleTabs = null; // invalidate cache
 
     // Invalidate hovered tab state tracking for this closing tab.
     if (this.tabContainer._hoveredTab == aTab)
       aTab._mouseleave();
 
     if (newTab)
       this.addTrustedTab(BROWSER_NEW_TAB_URL, {
         skipAnimation: true,
@@ -3087,17 +3083,16 @@ window._gBrowser = {
       // like `getBrowserContainer` expect the browser to be parented.
       browser.destroy();
     }
 
     var wasPinned = aTab.pinned;
 
     // Remove the tab ...
     aTab.remove();
-    this._invalidateCachedTabs();
 
     // Update hashiddentabs if this tab was hidden.
     if (aTab.hidden)
       this.tabContainer._updateHiddenTabsStatus();
 
     // ... and fix up the _tPos properties immediately.
     for (let i = aTab._tPos; i < this.tabs.length; i++)
       this.tabs[i]._tPos = i;
@@ -3180,26 +3175,27 @@ window._gBrowser = {
     // Switch to a visible tab unless there aren't any others remaining
     let remainingTabs = this.visibleTabs;
     let numTabs = remainingTabs.length;
     if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) {
       remainingTabs = Array.prototype.filter.call(this.tabs, tab => !tab.closing);
     }
 
     // Try to find a remaining tab that comes after the given tab
-    let tab = this.tabContainer.findNextTab(aTab, {
-      direction: 1,
-      filter: _tab => remainingTabs.includes(_tab),
-    });
+    let tab = aTab;
+    do {
+      tab = tab.nextElementSibling;
+    } while (tab && !remainingTabs.includes(tab));
 
     if (!tab) {
-      tab = this.tabContainer.findNextTab(aTab, {
-        direction: -1,
-        filter: _tab => remainingTabs.includes(_tab),
-      });
+      tab = aTab;
+
+      do {
+        tab = tab.previousElementSibling;
+      } while (tab && !remainingTabs.includes(tab));
     }
 
     return tab;
   },
 
   _blurTab(aTab) {
     this.selectedTab = this._findTabToBlurTo(aTab);
   },
@@ -3541,17 +3537,17 @@ window._gBrowser = {
 
     this.tabContainer._updateHiddenTabsStatus();
     this.tabContainer._handleTabSelect(true);
   },
 
   showTab(aTab) {
     if (aTab.hidden) {
       aTab.removeAttribute("hidden");
-      this._invalidateCachedTabs();
+      this._visibleTabs = null; // invalidate cache
 
       this.tabContainer._updateCloseButtons();
       this.tabContainer._updateHiddenTabsStatus();
 
       this.tabContainer._setPositionalAttributes();
 
       let event = document.createEvent("Events");
       event.initEvent("TabShow", true, false);
@@ -3559,17 +3555,17 @@ window._gBrowser = {
       SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
     }
   },
 
   hideTab(aTab, aSource) {
     if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
         !aTab.closing && !aTab._sharingState) {
       aTab.setAttribute("hidden", "true");
-      this._invalidateCachedTabs();
+      this._visibleTabs = null; // invalidate cache
 
       this.tabContainer._updateCloseButtons();
       this.tabContainer._updateHiddenTabsStatus();
 
       this.tabContainer._setPositionalAttributes();
 
       // Splice this tab out of any lines of succession before any events are
       // dispatched.
@@ -3726,19 +3722,22 @@ window._gBrowser = {
     if (!aKeepRelatedTabs) {
       this._lastRelatedTabMap = new WeakMap();
     }
 
     let wasFocused = (document.activeElement == this.selectedTab);
 
     aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;
 
-    let neighbor = this.tabs[aIndex] || null;
-    this._invalidateCachedTabs();
-    this.tabContainer.insertBefore(aTab, neighbor);
+    // invalidate cache
+    this._visibleTabs = null;
+
+    // use .item() instead of [] because dragging to the end of the strip goes out of
+    // bounds: .item() returns null (so it acts like appendChild), but [] throws
+    this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex));
     this._updateTabsAfterInsert();
 
     if (wasFocused)
       this.selectedTab.focus();
 
     this.tabContainer._handleTabSelect(true);
 
     if (aTab.pinned)
@@ -3747,20 +3746,19 @@ window._gBrowser = {
     this.tabContainer._setPositionalAttributes();
 
     var evt = document.createEvent("UIEvents");
     evt.initUIEvent("TabMove", true, false, window, oldPosition);
     aTab.dispatchEvent(evt);
   },
 
   moveTabForward() {
-    let nextTab = this.tabContainer.findNextTab(this.selectedTab, {
-      direction: 1,
-      filter: tab => !tab.hidden,
-    });
+    let nextTab = this.selectedTab.nextElementSibling;
+    while (nextTab && nextTab.hidden)
+      nextTab = nextTab.nextElementSibling;
 
     if (nextTab)
       this.moveTabTo(this.selectedTab, nextTab._tPos);
     else if (this.arrowKeysShouldWrap)
       this.moveTabToStart();
   },
 
   /**
@@ -3794,17 +3792,17 @@ window._gBrowser = {
 
     if (aTab.hasAttribute("usercontextid")) {
       // new tab must have the same usercontextid as the old one
       params.userContextId = aTab.getAttribute("usercontextid");
     }
     let newTab = this.addWebTab("about:blank", params);
     let newBrowser = this.getBrowserForTab(newTab);
 
-    aTab.container._finishAnimateTabMove();
+    aTab.parentNode._finishAnimateTabMove();
 
     if (!createLazyBrowser) {
       // Stop the about:blank load.
       newBrowser.stop();
       // Make sure it has a docshell.
       newBrowser.docShell;
     }
 
@@ -3817,20 +3815,19 @@ window._gBrowser = {
     if (aSelectTab) {
       this.selectedTab = newTab;
     }
 
     return newTab;
   },
 
   moveTabBackward() {
-    let previousTab = this.tabContainer.findNextTab(this.selectedTab, {
-      direction: -1,
-      filter: tab => !tab.hidden,
-    });
+    let previousTab = this.selectedTab.previousElementSibling;
+    while (previousTab && previousTab.hidden)
+      previousTab = previousTab.previousElementSibling;
 
     if (previousTab)
       this.moveTabTo(this.selectedTab, previousTab._tPos);
     else if (this.arrowKeysShouldWrap)
       this.moveTabToEnd();
   },
 
   moveTabToStart() {
@@ -3891,17 +3888,17 @@ window._gBrowser = {
   /**
    * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
    */
   addRangeToMultiSelectedTabs(aTab1, aTab2) {
     if (aTab1 == aTab2) {
       return;
     }
 
-    const tabs = this.visibleTabs;
+    const tabs = this._visibleTabs;
     const indexOfTab1 = tabs.indexOf(aTab1);
     const indexOfTab2 = tabs.indexOf(aTab2);
 
     const [lowerIndex, higherIndex] = indexOfTab1 < indexOfTab2 ?
       [indexOfTab1, indexOfTab2] : [indexOfTab2, indexOfTab1];
 
     for (let i = lowerIndex; i <= higherIndex; i++) {
       this.addToMultiSelectedTabs(tabs[i], true);
@@ -4452,17 +4449,16 @@ window._gBrowser = {
 
     // We want panel IDs to be globally unique, that's why we include the
     // window ID. We switched to a monotonic counter as Date.now() lead
     // to random failures because of colliding IDs.
     return "panel-" + outerID + "-" + (++this._uniquePanelIDCounter);
   },
 
   destroy() {
-    this.tabContainer.destroy();
     Services.obs.removeObserver(this, "contextual-identity-updated");
 
     for (let tab of this.tabs) {
       let browser = tab.linkedBrowser;
       if (browser.registeredOpenURI) {
         let userContextId = browser.getAttribute("usercontextid") || 0;
         this.UrlbarProviderOpenTabs.unregisterOpenTab(browser.registeredOpenURI.spec,
                                                       userContextId);
@@ -5451,22 +5447,18 @@ var TabContextMenu = {
     let allSelectedTabsAdjacent = selectedTabs.every((element, index, array) => {
       return array.length > index + 1 ? element._tPos + 1 == array[index + 1]._tPos : true;
     });
     let contextTabIsSelected = this.contextTab.multiselected;
     let visibleTabs = gBrowser.visibleTabs;
     let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
     let tabsToMove = contextTabIsSelected ? selectedTabs : [this.contextTab];
     let lastTabToMove = tabsToMove[tabsToMove.length - 1];
-
-    let isLastPinnedTab = false;
-    if (lastTabToMove.pinned) {
-      let sibling = gBrowser.tabContainer.findNextTab(lastTabToMove);
-      isLastPinnedTab = !sibling || !sibling.pinned;
-    }
+    let isLastPinnedTab = lastTabToMove.pinned &&
+      (!lastTabToMove.nextElementSibling || !lastTabToMove.nextElementSibling.pinned);
     contextMoveTabToEnd.disabled = (lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
                                    allSelectedTabsAdjacent;
     let contextMoveTabToStart = document.getElementById("context_moveToStart");
     let isFirstTab = tabsToMove[0] == visibleTabs[0] ||
                      tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
     contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
 
     // Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
rename from browser/base/content/tabbrowser-tabs.js
rename to browser/base/content/tabbrowser.xml
--- a/browser/base/content/tabbrowser-tabs.js
+++ b/browser/base/content/tabbrowser.xml
@@ -1,1828 +1,1915 @@
-/* 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/. */
+<?xml version="1.0"?>
+
+<!-- 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/. -->
 
-/* eslint-env mozilla/browser-window */
+<bindings id="tabBrowserBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
+  <binding id="tabbrowser-tabs"
+           extends="chrome://global/content/bindings/tabbox.xml#tabs">
+    <content>
+      <xul:hbox class="tab-drop-indicator-box">
+        <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/>
+      </xul:hbox>
+      <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1"
+                          style="min-width: 1px;"
+                          clicktoscroll="true"
+                          class="tabbrowser-arrowscrollbox">
+<!--
+ This is a hack to circumvent bug 472020, otherwise the tabs show up on the
+ right of the newtab button.
+-->
+        <children includes="tab"/>
+<!--
+  This is to ensure anything extensions put here will go before the newtab
+  button, necessary due to the previous hack.
+-->
+        <children/>
+        <xul:toolbarbutton class="tabs-newtab-button toolbarbutton-1"
+                           anonid="tabs-newtab-button"
+                           command="cmd_newNavigatorTab"
+                           onclick="checkForMiddleClick(this, event);"
+                           />
+        <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
+                    style="width: 0;"/>
+      </xul:arrowscrollbox>
+    </content>
+
+    <implementation implements="nsIObserver">
+      <constructor>
+        <![CDATA[
+          this._tabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
+          this._hiddenSoundPlayingTabs = new Set();
+
+          let strId = PrivateBrowsingUtils.isWindowPrivate(window) ?
+              "emptyPrivateTabTitle" : "emptyTabTitle";
+          this.emptyTabTitle = gTabBrowserBundle.GetStringFromName("tabs." + strId);
+
+          var tab = this.firstElementChild;
+          tab.label = this.emptyTabTitle;
+
+          let newTabButton = document.getAnonymousElementByAttribute(
+            this, "anonid", "tabs-newtab-button");
+          newTabButton.setAttribute("tooltiptext", GetDynamicShortcutTooltipText("tabs-newtab-button"));
+
+          window.addEventListener("resize", this);
+
+          Services.prefs.addObserver("privacy.userContext", this);
+          this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
+
+          XPCOMUtils.defineLazyPreferenceGetter(this, "_tabMinWidthPref",
+            "browser.tabs.tabMinWidth", null,
+            (pref, prevValue, newValue) => this._tabMinWidth = newValue,
+            newValue => {
+              const LIMIT = 50;
+              return Math.max(newValue, LIMIT);
+            },
+          );
+
+          this._tabMinWidth = this._tabMinWidthPref;
 
-"use strict";
+          XPCOMUtils.defineLazyPreferenceGetter(this, "_multiselectEnabledPref",
+            "browser.tabs.multiselect", null,
+            (pref, prevValue, newValue) => this._multiselectEnabled = newValue);
+          this._multiselectEnabled = this._multiselectEnabledPref;
+
+          this._setPositionalAttributes();
+
+          CustomizableUI.addListener(this);
+          this._updateNewTabVisibility();
+          this._initializeArrowScrollbox();
+
+          XPCOMUtils.defineLazyPreferenceGetter(this, "_closeTabByDblclick",
+            "browser.tabs.closeTabByDblclick", false);
+
+          if (gMultiProcessBrowser) {
+            this.tabbox.tabpanels.setAttribute("async", "true");
+          }
+        ]]>
+      </constructor>
+
+      <destructor>
+        <![CDATA[
+          Services.prefs.removeObserver("privacy.userContext", this);
+
+          CustomizableUI.removeListener(this);
+        ]]>
+      </destructor>
+
+      <field name="tabbox" readonly="true">
+        document.getElementById("tabbrowser-tabbox");
+      </field>
+
+      <field name="contextMenu" readonly="true">
+        document.getElementById("tabContextMenu");
+      </field>
 
-// This is loaded into all browser windows. Wrap in a block to prevent
-// leaking to window scope.
-{
-class MozTabbrowserTabs extends MozElements.TabsBase {
-  constructor() {
-    super();
+      <field name="arrowScrollbox">
+        document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
+      </field>
+
+      <field name="_firstTab">null</field>
+      <field name="_lastTab">null</field>
+      <field name="_beforeSelectedTab">null</field>
+      <field name="_beforeHoveredTab">null</field>
+      <field name="_afterHoveredTab">null</field>
+      <field name="_hoveredTab">null</field>
+
+      <property name="_tabMinWidth">
+        <setter>
+          this.style.setProperty("--tab-min-width", val + "px");
+          return val;
+        </setter>
+      </property>
+
+      <property name="_multiselectEnabled">
+        <setter>
+          // Unlike boolean HTML attributes, the value of boolean ARIA attributes actually matters.
+          this.setAttribute("aria-multiselectable", !!val);
+          return val;
+        </setter>
+        <getter>
+          return this.getAttribute("aria-multiselectable") == "true";
+        </getter>
+      </property>
+
+      <method name="_initializeArrowScrollbox">
+        <body><![CDATA[
+          let arrowScrollbox = this.arrowScrollbox;
+          arrowScrollbox.addEventListener("underflow", event => {
+            // Ignore underflow events:
+            // - from nested scrollable elements
+            // - for vertical orientation
+            // - corresponding to an overflow event that we ignored
+            if (event.originalTarget != arrowScrollbox.scrollbox ||
+                event.detail == 0 ||
+                !this.hasAttribute("overflow")) {
+              return;
+            }
 
-    this.addEventListener("TabSelect", this);
-    this.addEventListener("TabClose", this);
-    this.addEventListener("TabAttrModified", this);
-    this.addEventListener("TabHide", this);
-    this.addEventListener("TabShow", this);
-    this.addEventListener("transitionend", this);
-    this.addEventListener("dblclick", this);
-    this.addEventListener("click", this);
-    this.addEventListener("click", this, true);
-    this.addEventListener("keydown", this, { mozSystemGroup: true });
-    this.addEventListener("dragstart", this);
-    this.addEventListener("dragover", this);
-    this.addEventListener("drop", this);
-    this.addEventListener("dragend", this);
-    this.addEventListener("dragexit", this);
-  }
+            this.removeAttribute("overflow");
+
+            if (this._lastTabClosedByMouse) {
+              this._expandSpacerBy(this._scrollButtonWidth);
+            }
+
+            for (let tab of Array.from(gBrowser._removingTabs)) {
+              gBrowser.removeTab(tab);
+            }
+
+            this._positionPinnedTabs();
+          }, true);
+
+          arrowScrollbox.addEventListener("overflow", event => {
+            // Ignore overflow events:
+            // - from nested scrollable elements
+            // - for vertical orientation
+            if (event.originalTarget != arrowScrollbox.scrollbox ||
+                event.detail == 0) {
+              return;
+            }
+
+            this.setAttribute("overflow", "true");
+            this._positionPinnedTabs();
+            this._handleTabSelect(true);
+          });
+
+          // Override scrollbox.xml method, since our scrollbox's children are
+          // inherited from the scrollbox binding parent (this).
+          arrowScrollbox._getScrollableElements = () => {
+            return Array.prototype.filter.call(this.children, arrowScrollbox._canScrollToElement);
+          };
+          arrowScrollbox._canScrollToElement = tab => {
+            return !tab._pinnedUnscrollable && !tab.hidden;
+          };
+        ]]></body>
+      </method>
+
+      <method name="observe">
+        <parameter name="aSubject"/>
+        <parameter name="aTopic"/>
+        <parameter name="aData"/>
+        <body><![CDATA[
+          switch (aTopic) {
+            case "nsPref:changed":
+              // This is has to deal with changes in
+              // privacy.userContext.enabled and
+              // privacy.userContext.longPressBehavior.
+              let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled")
+                                        && !PrivateBrowsingUtils.isWindowPrivate(window);
+
+              // This pref won't change so often, so just recreate the menu.
+              let longPressBehavior = Services.prefs.getIntPref("privacy.userContext.longPressBehavior");
+
+              // If longPressBehavior pref is set to 0 (or any invalid value)
+              // long press menu is disabled.
+              if (containersEnabled && (longPressBehavior <= 0 || longPressBehavior > 2)) {
+                containersEnabled = false;
+              }
+
+              // There are separate "new tab" buttons for when the tab strip
+              // is overflowed and when it is not.  Attach the long click
+              // popup to both of them.
+              const newTab = document.getElementById("new-tab-button");
+              const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button");
+
+              for (let parent of [newTab, newTab2]) {
+                if (!parent)
+                  continue;
+
+                gClickAndHoldListenersOnElement.remove(parent);
+                parent.removeAttribute("type");
+                if (parent.menupopup) {
+                  parent.menupopup.remove();
+                }
 
-  init() {
-    this.arrowScrollbox = this.querySelector("arrowscrollbox");
+                if (containersEnabled) {
+                  let popup = document.createElementNS(
+                                "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+                                "menupopup");
+                  if (parent.id) {
+                    popup.id = "newtab-popup";
+                  } else {
+                    popup.setAttribute("anonid", "newtab-popup");
+                  }
+                  popup.className = "new-tab-popup";
+                  popup.setAttribute("position", "after_end");
+                  popup.addEventListener("popupshowing", event => {
+                    createUserContextMenu(event, {
+                      useAccessKeys: false,
+                      showDefaultTab: Services.prefs.getIntPref("privacy.userContext.longPressBehavior") == 1,
+                    });
+                  });
+                  parent.prepend(popup);
+
+                  // longPressBehavior == 2 means that the menu is shown after X
+                  // millisecs. Otherwise, with 1, the menu is open immediatelly.
+                  if (longPressBehavior == 2) {
+                    gClickAndHoldListenersOnElement.add(parent);
+                  }
+
+                  parent.setAttribute("type", "menu");
+                }
+              }
+
+              break;
+          }
+        ]]></body>
+      </method>
 
-    this.baseConnect();
+      <property name="_isCustomizing" readonly="true">
+        <getter><![CDATA[
+          return document.documentElement.getAttribute("customizing") == "true";
+        ]]></getter>
+      </property>
+
+      <method name="_getVisibleTabs">
+        <body><![CDATA[
+          // Cannot access gBrowser before it's initialized.
+          if (!gBrowser) {
+            return [ this.firstElementChild ];
+          }
+
+          return gBrowser.visibleTabs;
+        ]]></body>
+      </method>
+
+      <method name="_setPositionalAttributes">
+        <body><![CDATA[
+          let visibleTabs = this._getVisibleTabs();
+          if (!visibleTabs.length) {
+            return;
+          }
+          let selectedTab = this.selectedItem;
+          let selectedIndex = visibleTabs.indexOf(selectedTab);
+          if (this._beforeSelectedTab) {
+            this._beforeSelectedTab.removeAttribute("beforeselected-visible");
+          }
+
+          if (selectedTab.closing || selectedIndex <= 0) {
+            this._beforeSelectedTab = null;
+          } else {
+            let beforeSelectedTab = visibleTabs[selectedIndex - 1];
+            let separatedByScrollButton = this.getAttribute("overflow") == "true" &&
+              beforeSelectedTab.pinned && !selectedTab.pinned;
+            if (!separatedByScrollButton) {
+              this._beforeSelectedTab = beforeSelectedTab;
+              this._beforeSelectedTab.setAttribute("beforeselected-visible",
+                                                   "true");
+            }
+          }
 
-    this._firstTab = null;
-    this._lastTab = null;
-    this._beforeSelectedTab = null;
-    this._beforeHoveredTab = null;
-    this._afterHoveredTab = null;
-    this._hoveredTab = null;
-    this._blockDblClick = false;
-    this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
-    this._dragOverDelay = 350;
-    this._dragTime = 0;
-    this._closeButtonsUpdatePending = false;
-    this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
-    this._tabDefaultMaxWidth = NaN;
-    this._lastTabClosedByMouse = false;
-    this._hasTabTempMaxWidth = false;
-    this._scrollButtonWidth = 0;
-    this._lastNumPinned = 0;
-    this._pinnedTabsLayoutCache = null;
-    this._animateElement = this.arrowScrollbox._scrollButtonDown;
-    this._tabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
-    this._hiddenSoundPlayingTabs = new Set();
+          if (this._firstTab)
+            this._firstTab.removeAttribute("first-visible-tab");
+          this._firstTab = visibleTabs[0];
+          this._firstTab.setAttribute("first-visible-tab", "true");
+          if (this._lastTab)
+            this._lastTab.removeAttribute("last-visible-tab");
+          this._lastTab = visibleTabs[visibleTabs.length - 1];
+          this._lastTab.setAttribute("last-visible-tab", "true");
+
+          let hoveredTab = this._hoveredTab;
+          if (hoveredTab) {
+            hoveredTab._mouseleave();
+          }
+          hoveredTab = this.querySelector("tab:hover");
+          if (hoveredTab) {
+            hoveredTab._mouseenter();
+          }
+
+          // Update before-multiselected attributes.
+          // gBrowser may not be initialized yet, so avoid using it
+          for (let i = 0; i < visibleTabs.length - 1; i++) {
+            let tab = visibleTabs[i];
+            let nextTab = visibleTabs[i + 1];
+            tab.removeAttribute("before-multiselected");
+            if (nextTab.multiselected) {
+              tab.setAttribute("before-multiselected", "true");
+            }
+          }
+        ]]></body>
+      </method>
+
+      <field name="_blockDblClick">false</field>
+
+      <field name="_tabDropIndicator">
+        document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator");
+      </field>
+
+      <field name="_dragOverDelay">350</field>
+      <field name="_dragTime">0</field>
+
+      <field name="_closeButtonsUpdatePending">false</field>
+      <method name="_updateCloseButtons">
+        <body><![CDATA[
+          // If we're overflowing, tabs are at their minimum widths.
+          if (this.getAttribute("overflow") == "true") {
+            this.setAttribute("closebuttons", "activetab");
+            return;
+          }
+
+          if (this._closeButtonsUpdatePending) {
+            return;
+          }
+          this._closeButtonsUpdatePending = true;
+
+          // Wait until after the next paint to get current layout data from
+          // getBoundsWithoutFlushing.
+          window.requestAnimationFrame(() => {
+            window.requestAnimationFrame(() => {
+              this._closeButtonsUpdatePending = false;
 
-    let strId = PrivateBrowsingUtils.isWindowPrivate(window) ?
-                "emptyPrivateTabTitle" : "emptyTabTitle";
-    this.emptyTabTitle = gTabBrowserBundle.GetStringFromName("tabs." + strId);
+              // The scrollbox may have started overflowing since we checked
+              // overflow earlier, so check again.
+              if (this.getAttribute("overflow") == "true") {
+                this.setAttribute("closebuttons", "activetab");
+                return;
+              }
+
+              // Check if tab widths are below the threshold where we want to
+              // remove close buttons from background tabs so that people don't
+              // accidentally close tabs by selecting them.
+              let rect = ele => {
+                return window.windowUtils.getBoundsWithoutFlushing(ele);
+              };
+              let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
+              if (tab && rect(tab).width <= this._tabClipWidth) {
+                this.setAttribute("closebuttons", "activetab");
+              } else {
+                this.removeAttribute("closebuttons");
+              }
+            });
+          });
+        ]]></body>
+      </method>
+
+      <method name="_updateHiddenTabsStatus">
+        <body><![CDATA[
+          if (gBrowser.visibleTabs.length < gBrowser.tabs.length) {
+            this.setAttribute("hashiddentabs", "true");
+          } else {
+            this.removeAttribute("hashiddentabs");
+          }
+        ]]></body>
+      </method>
 
-    var tab = this.allTabs[0];
-    tab.label = this.emptyTabTitle;
+      <method name="_handleTabSelect">
+        <parameter name="aInstant"/>
+        <body><![CDATA[
+          let selectedTab = this.selectedItem;
+          if (this.getAttribute("overflow") == "true")
+            this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
+
+          selectedTab._notselectedsinceload = false;
+        ]]></body>
+      </method>
+
+      <field name="_closingTabsSpacer">
+        document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer");
+      </field>
 
-    this.newTabButton.setAttribute("tooltiptext", GetDynamicShortcutTooltipText("tabs-newtab-button"));
+      <field name="_tabDefaultMaxWidth">NaN</field>
+      <field name="_lastTabClosedByMouse">false</field>
+      <field name="_hasTabTempMaxWidth">false</field>
+      <field name="_scrollButtonWidth">0</field>
 
-    window.addEventListener("resize", this);
+      <!-- Try to keep the active tab's close button under the mouse cursor -->
+      <method name="_lockTabSizing">
+        <parameter name="aTab"/>
+        <parameter name="aTabWidth"/>
+        <body><![CDATA[
+          let tabs = this._getVisibleTabs();
+          if (!tabs.length) {
+            return;
+          }
+
+          var isEndTab = (aTab._tPos > tabs[tabs.length - 1]._tPos);
+
+          if (!this._tabDefaultMaxWidth) {
+            this._tabDefaultMaxWidth =
+              parseFloat(window.getComputedStyle(aTab).maxWidth);
+          }
+          this._lastTabClosedByMouse = true;
+          this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox._scrollButtonDown).width;
 
-    this.boundObserve = (...args) => this.observe(...args);
-    Services.prefs.addObserver("privacy.userContext", this.boundObserve);
-    this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
+          if (this.getAttribute("overflow") == "true") {
+            // Don't need to do anything if we're in overflow mode and aren't scrolled
+            // all the way to the right, or if we're closing the last tab.
+            if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
+              return;
+            }
+            // If the tab has an owner that will become the active tab, the owner will
+            // be to the left of it, so we actually want the left tab to slide over.
+            // This can't be done as easily in non-overflow mode, so we don't bother.
+            if (aTab.owner) {
+              return;
+            }
+            this._expandSpacerBy(aTabWidth);
+          } else { // non-overflow mode
+            // Locking is neither in effect nor needed, so let tabs expand normally.
+            if (isEndTab && !this._hasTabTempMaxWidth) {
+              return;
+            }
+            let numPinned = gBrowser._numPinnedTabs;
+            // Force tabs to stay the same width, unless we're closing the last tab,
+            // which case we need to let them expand just enough so that the overall
+            // tabbar width is the same.
+            if (isEndTab) {
+              let numNormalTabs = tabs.length - numPinned;
+              aTabWidth = aTabWidth * (numNormalTabs + 1) / numNormalTabs;
+              if (aTabWidth > this._tabDefaultMaxWidth) {
+                aTabWidth = this._tabDefaultMaxWidth;
+              }
+            }
+            aTabWidth += "px";
+            let tabsToReset = [];
+            for (let i = numPinned; i < tabs.length; i++) {
+              let tab = tabs[i];
+              tab.style.setProperty("max-width", aTabWidth, "important");
+              if (!isEndTab) { // keep tabs the same width
+                tab.style.transition = "none";
+                tabsToReset.push(tab);
+              }
+            }
 
-    XPCOMUtils.defineLazyPreferenceGetter(this, "_tabMinWidthPref",
-                                          "browser.tabs.tabMinWidth", null,
-                                          (pref, prevValue, newValue) => this._tabMinWidth = newValue,
-                                          newValue => {
-                                            const LIMIT = 50;
-                                            return Math.max(newValue, LIMIT);
-                                          },
-    );
+            if (tabsToReset.length) {
+              window.promiseDocumentFlushed(() => {}).then(() => {
+                window.requestAnimationFrame(() => {
+                  for (let tab of tabsToReset) {
+                    tab.style.transition = "";
+                  }
+                });
+              });
+            }
+
+            this._hasTabTempMaxWidth = true;
+            gBrowser.addEventListener("mousemove", this);
+            window.addEventListener("mouseout", this);
+          }
+        ]]></body>
+      </method>
 
-    this._tabMinWidth = this._tabMinWidthPref;
+      <method name="_expandSpacerBy">
+        <parameter name="pixels"/>
+        <body><![CDATA[
+          let spacer = this._closingTabsSpacer;
+          spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
+          this.setAttribute("using-closing-tabs-spacer", "true");
+          gBrowser.addEventListener("mousemove", this);
+          window.addEventListener("mouseout", this);
+        ]]></body>
+      </method>
+
+      <method name="_unlockTabSizing">
+        <body><![CDATA[
+          gBrowser.removeEventListener("mousemove", this);
+          window.removeEventListener("mouseout", this);
+
+          if (this._hasTabTempMaxWidth) {
+            this._hasTabTempMaxWidth = false;
+            let tabs = this._getVisibleTabs();
+            for (let i = 0; i < tabs.length; i++) {
+              tabs[i].style.maxWidth = "";
+            }
+          }
+
+          if (this.hasAttribute("using-closing-tabs-spacer")) {
+            this.removeAttribute("using-closing-tabs-spacer");
+            this._closingTabsSpacer.style.width = 0;
+          }
+        ]]></body>
+      </method>
 
-    XPCOMUtils.defineLazyPreferenceGetter(this, "_multiselectEnabledPref",
-                                          "browser.tabs.multiselect", null,
-                                          (pref, prevValue, newValue) => this._multiselectEnabled = newValue);
-    this._multiselectEnabled = this._multiselectEnabledPref;
+      <method name="uiDensityChanged">
+        <body><![CDATA[
+          this._positionPinnedTabs();
+          this._updateCloseButtons();
+          this._handleTabSelect(true);
+        ]]></body>
+      </method>
+
+      <field name="_lastNumPinned">0</field>
+      <field name="_pinnedTabsLayoutCache">null</field>
+      <method name="_positionPinnedTabs">
+        <body><![CDATA[
+          let numPinned = gBrowser._numPinnedTabs;
+          let doPosition = this.getAttribute("overflow") == "true" &&
+                           this._getVisibleTabs().length > numPinned &&
+                           numPinned > 0;
+
+          if (doPosition) {
+            this.setAttribute("positionpinnedtabs", "true");
 
-    this._setPositionalAttributes();
+            let layoutData = this._pinnedTabsLayoutCache;
+            let uiDensity = document.documentElement.getAttribute("uidensity");
+            if (!layoutData ||
+                layoutData.uiDensity != uiDensity) {
+              let arrowScrollbox = this.arrowScrollbox;
+              layoutData = this._pinnedTabsLayoutCache = {
+                uiDensity,
+                pinnedTabWidth: this.children[0].getBoundingClientRect().width,
+                scrollButtonWidth: arrowScrollbox._scrollButtonDown.getBoundingClientRect().width,
+              };
+            }
 
-    CustomizableUI.addListener(this);
-    this._updateNewTabVisibility();
-    this._initializeArrowScrollbox();
+            let width = 0;
+            for (let i = numPinned - 1; i >= 0; i--) {
+              let tab = this.children[i];
+              width += layoutData.pinnedTabWidth;
+              tab.style.setProperty("margin-inline-start",
+                -(width + layoutData.scrollButtonWidth) + "px", "important");
+              tab._pinnedUnscrollable = true;
+            }
+            this.style.paddingInlineStart = width + "px";
+          } else {
+            this.removeAttribute("positionpinnedtabs");
 
-    XPCOMUtils.defineLazyPreferenceGetter(this, "_closeTabByDblclick",
-                                          "browser.tabs.closeTabByDblclick", false);
+            for (let i = 0; i < numPinned; i++) {
+              let tab = this.children[i];
+              tab.style.marginInlineStart = "";
+              tab._pinnedUnscrollable = false;
+            }
+
+            this.style.paddingInlineStart = "";
+          }
+
+          if (this._lastNumPinned != numPinned) {
+            this._lastNumPinned = numPinned;
+            this._handleTabSelect(true);
+          }
+        ]]></body>
+      </method>
+
+      <method name="_animateTabMove">
+        <parameter name="event"/>
+        <body><![CDATA[
+          let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+          let movingTabs = draggedTab._dragData.movingTabs;
 
-    if (gMultiProcessBrowser) {
-      this.tabbox.tabpanels.setAttribute("async", "true");
-    }
-  }
+          if (this.getAttribute("movingtab") != "true") {
+            this.setAttribute("movingtab", "true");
+            gNavToolbox.setAttribute("movingtab", "true");
+            if (!draggedTab.multiselected)
+              this.selectedItem = draggedTab;
+          }
+
+          if (!("animLastScreenX" in draggedTab._dragData))
+            draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
 
-  on_TabSelect(event) {
-    this._handleTabSelect();
-  }
+          let screenX = event.screenX;
+          if (screenX == draggedTab._dragData.animLastScreenX)
+            return;
+
+          // Direction of the mouse movement.
+          let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
+
+          draggedTab._dragData.animLastScreenX = screenX;
+
+          let pinned = draggedTab.pinned;
+          let numPinned = gBrowser._numPinnedTabs;
+          let tabs = this._getVisibleTabs()
+                         .slice(pinned ? 0 : numPinned,
+                                pinned ? numPinned : undefined);
 
-  on_TabClose(event) {
-    this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
-  }
+          if (RTL_UI) {
+            tabs.reverse();
+            // Copy moving tabs array to avoid infinite reversing.
+            movingTabs = [...movingTabs].reverse();
+          }
+          let tabWidth = draggedTab.getBoundingClientRect().width;
+          let shiftWidth = tabWidth * movingTabs.length;
+          draggedTab._dragData.tabWidth = tabWidth;
+
+          // Move the dragged tab based on the mouse position.
+
+          let leftTab = tabs[0];
+          let rightTab = tabs[tabs.length - 1];
+          let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
+          let leftMovingTabScreenX = movingTabs[0].screenX;
+          let translateX = screenX - draggedTab._dragData.screenX;
+          if (!pinned) {
+            translateX += this.arrowScrollbox.scrollbox.scrollLeft - draggedTab._dragData.scrollX;
+          }
+          let leftBound = leftTab.screenX - leftMovingTabScreenX;
+          let rightBound = (rightTab.screenX + rightTab.getBoundingClientRect().width) -
+                           (rightMovingTabScreenX + tabWidth);
+          translateX = Math.min(Math.max(translateX, leftBound), rightBound);
+
+          for (let tab of movingTabs) {
+            tab.style.transform = "translateX(" + translateX + "px)";
+          }
+
+          draggedTab._dragData.translateX = translateX;
 
-  on_TabAttrModified(event) {
-    if (event.detail.changed.includes("soundplaying") && event.target.hidden) {
-      this._hiddenSoundPlayingStatusChanged(event.target);
-    }
-  }
+          // Determine what tab we're dragging over.
+          // * Single tab dragging: Point of reference is the center of the dragged tab. If that
+          //   point touches a background tab, the dragged tab would take that
+          //   tab's position when dropped.
+          // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
+          //   points of reference (center of tabs on the extremities). When
+          //   mouse is moving from left to right, the right reference gets activated,
+          //   otherwise the left reference will be used. Everything else works the same
+          //   as single tab dragging.
+          // * We're doing a binary search in order to reduce the amount of
+          //   tabs we need to check.
+
+          tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
+          let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
+          let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
+          let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
+          let newIndex = -1;
+          let oldIndex = "animDropIndex" in draggedTab._dragData ?
+                         draggedTab._dragData.animDropIndex : movingTabs[0]._tPos;
+          let low = 0;
+          let high = tabs.length - 1;
+          while (low <= high) {
+            let mid = Math.floor((low + high) / 2);
+            if (tabs[mid] == draggedTab && ++mid > high)
+              break;
+            screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
+            if (screenX > tabCenter) {
+              high = mid - 1;
+            } else if (screenX + tabs[mid].getBoundingClientRect().width < tabCenter) {
+              low = mid + 1;
+            } else {
+              newIndex = tabs[mid]._tPos;
+              break;
+            }
+          }
+          if (newIndex >= oldIndex)
+            newIndex++;
+          if (newIndex < 0 || newIndex == oldIndex)
+            return;
+          draggedTab._dragData.animDropIndex = newIndex;
+
+          // Shift background tabs to leave a gap where the dragged tab
+          // would currently be dropped.
 
-  on_TabHide(event) {
-    if (event.target.soundPlaying) {
-      this._hiddenSoundPlayingStatusChanged(event.target);
-    }
-  }
+          for (let tab of tabs) {
+            if (tab != draggedTab) {
+              let shift = getTabShift(tab, newIndex);
+              tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
+            }
+          }
+
+          function getTabShift(tab, dropIndex) {
+            if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
+              return (RTL_UI ? -shiftWidth : shiftWidth);
+            if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
+              return (RTL_UI ? shiftWidth : -shiftWidth);
+            return 0;
+          }
+        ]]></body>
+      </method>
+
+      <method name="_finishAnimateTabMove">
+        <body><![CDATA[
+          if (this.getAttribute("movingtab") != "true") {
+            return;
+          }
+
+          for (let tab of this._getVisibleTabs()) {
+            tab.style.transform = "";
+          }
+
+          this.removeAttribute("movingtab");
+          gNavToolbox.removeAttribute("movingtab");
+
+          this._handleTabSelect();
+        ]]></body>
+      </method>
 
-  on_TabShow(event) {
-    if (event.target.soundPlaying) {
-      this._hiddenSoundPlayingStatusChanged(event.target);
-    }
-  }
+      <!--  Regroup all selected tabs around the
+            tab in param  -->
+      <method name="_groupSelectedTabs">
+        <parameter name="tab"/>
+        <body><![CDATA[
+          let draggedTabPos = tab._tPos;
+          let selectedTabs = gBrowser.selectedTabs;
+          let animate = gBrowser.animationsEnabled;
+
+          tab.groupingTabsData = {
+            finished: !animate,
+          };
+
+
+          // Animate left selected tabs
+
+          let insertAtPos = draggedTabPos - 1;
+          for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
+            let movingTab = selectedTabs[i];
+            insertAtPos = newIndex(movingTab, insertAtPos);
 
-  on_transitionend(event) {
-    if (event.propertyName != "max-width") {
-      return;
-    }
+            if (animate) {
+              movingTab.groupingTabsData = {};
+              addAnimationData(movingTab, insertAtPos, "left");
+            } else {
+              gBrowser.moveTabTo(movingTab, insertAtPos);
+            }
+            insertAtPos--;
+          }
+
+          // Animate right selected tabs
+
+          insertAtPos = draggedTabPos + 1;
+          for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
+            let movingTab = selectedTabs[i];
+            insertAtPos = newIndex(movingTab, insertAtPos);
+
+            if (animate) {
+              movingTab.groupingTabsData = {};
+              addAnimationData(movingTab, insertAtPos, "right");
+            } else {
+              gBrowser.moveTabTo(movingTab, insertAtPos);
+            }
+            insertAtPos++;
+          }
 
-    let tab = event.target ? event.target.closest("tab") : null;
+          // Slide the relevant tabs to their new position.
+          for (let t of this._getVisibleTabs()) {
+            if (t.groupingTabsData && t.groupingTabsData.translateX) {
+              let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
+              t.style.transform = "translateX(" + translateX + "px)";
+            }
+          }
+
+          function newIndex(aTab, index) {
+            // Don't allow mixing pinned and unpinned tabs.
+            if (aTab.pinned) {
+              return Math.min(index, gBrowser._numPinnedTabs - 1);
+            }
+            return Math.max(index, gBrowser._numPinnedTabs);
+          }
+
+          function addAnimationData(movingTab, movingTabNewIndex, side) {
+            let movingTabOldIndex = movingTab._tPos;
+
+            if (movingTabOldIndex == movingTabNewIndex) {
+              // movingTab is already at the right position
+              // and thus don't need to be animated.
+              return;
+            }
+
+            let movingTabWidth = movingTab.getBoundingClientRect().width;
+            let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
+
+            movingTab.groupingTabsData.animate = true;
+            movingTab.setAttribute("tab-grouping", "true");
+
+            movingTab.groupingTabsData.translateX = shift;
 
-    if (tab.getAttribute("fadein") == "true") {
-      if (tab._fullyOpen) {
-        this._updateCloseButtons();
-      } else {
-        this._handleNewTab(tab);
-      }
-    } else if (tab.closing) {
-      gBrowser._endRemoveTab(tab);
-    }
+            let onTransitionEnd = transitionendEvent => {
+              if (transitionendEvent.propertyName != "transform" ||
+                  transitionendEvent.originalTarget != movingTab) {
+                return;
+              }
+              movingTab.removeEventListener("transitionend", onTransitionEnd);
+              movingTab.groupingTabsData.newIndex = movingTabNewIndex;
+              movingTab.groupingTabsData.animate = false;
+            };
+
+            movingTab.addEventListener("transitionend", onTransitionEnd);
+
+            // Add animation data for tabs between movingTab (selected
+            // tab moving towards the dragged tab) and draggedTab.
+            // Those tabs in the middle should move in
+            // the opposite direction of movingTab.
+
+            let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
+            let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
+
+            for (let i = lowerIndex + 1; i < higherIndex; i++) {
+              let middleTab = gBrowser.visibleTabs[i];
+
+              if (middleTab.pinned != movingTab.pinned) {
+                // Don't mix pinned and unpinned tabs
+                break;
+              }
+
+              if (middleTab.multiselected) {
+                // Skip because this selected tab should
+                // be shifted towards the dragged Tab.
+                continue;
+              }
+
+              if (!middleTab.groupingTabsData || !middleTab.groupingTabsData.translateX) {
+                middleTab.groupingTabsData = { translateX: 0};
+              }
+              if (side == "left") {
+                middleTab.groupingTabsData.translateX -= movingTabWidth;
+              } else {
+                middleTab.groupingTabsData.translateX += movingTabWidth;
+              }
 
-    let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
-    tab.dispatchEvent(evt);
-  }
+              middleTab.setAttribute("tab-grouping", "true");
+            }
+          }
+        ]]></body>
+      </method>
+
+      <method name="_finishGroupSelectedTabs">
+        <parameter name="tab"/>
+        <body><![CDATA[
+          if (!tab.groupingTabsData || tab.groupingTabsData.finished)
+            return;
+
+          tab.groupingTabsData.finished = true;
+
+          let selectedTabs = gBrowser.selectedTabs;
+          let tabIndex = selectedTabs.indexOf(tab);
+
+          // Moving left tabs
+          for (let i = tabIndex - 1; i > -1; i--) {
+            let movingTab = selectedTabs[i];
+            if (movingTab.groupingTabsData.newIndex) {
+              gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
+            }
+          }
+
+          // Moving right tabs
+          for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
+            let movingTab = selectedTabs[i];
+            if (movingTab.groupingTabsData.newIndex) {
+              gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
+            }
+          }
 
-  on_dblclick(event) {
-    // When the tabbar has an unified appearance with the titlebar
-    // and menubar, a double-click in it should have the same behavior
-    // as double-clicking the titlebar
-    if (TabsInTitlebar.enabled)
-      return;
+          for (let t of this._getVisibleTabs()) {
+            t.style.transform = "";
+            t.removeAttribute("tab-grouping");
+            delete t.groupingTabsData;
+          }
+        ]]></body>
+      </method>
+
+      <method name="_isGroupTabsAnimationOver">
+        <body><![CDATA[
+          for (let tab of gBrowser.selectedTabs) {
+            if (tab.groupingTabsData && tab.groupingTabsData.animate)
+              return false;
+          }
+          return true;
+        ]]></body>
+      </method>
 
-    if (event.button != 0 ||
-        event.originalTarget.localName != "scrollbox")
-      return;
+      <method name="handleEvent">
+        <parameter name="aEvent"/>
+        <body><![CDATA[
+          switch (aEvent.type) {
+            case "resize":
+              if (aEvent.target != window)
+                break;
+
+              this._updateCloseButtons();
+              this._handleTabSelect(true);
+              break;
+            case "mouseout":
+              // If the "related target" (the node to which the pointer went) is not
+              // a child of the current document, the mouse just left the window.
+              let relatedTarget = aEvent.relatedTarget;
+              if (relatedTarget && relatedTarget.ownerDocument == document)
+                break;
+            case "mousemove":
+              if (document.getElementById("tabContextMenu").state != "open")
+                this._unlockTabSizing();
+              break;
+          }
+        ]]></body>
+      </method>
+
+      <field name="_animateElement">
+        this.arrowScrollbox._scrollButtonDown;
+      </field>
 
-    if (!this._blockDblClick)
-      BrowserOpenTab();
+      <method name="_notifyBackgroundTab">
+        <parameter name="aTab"/>
+        <body><![CDATA[
+          if (aTab.pinned || aTab.hidden || this.getAttribute("overflow") != "true")
+            return;
+
+          this._lastTabToScrollIntoView = aTab;
+          if (!this._backgroundTabScrollPromise) {
+            this._backgroundTabScrollPromise = window.promiseDocumentFlushed(() => {
+              let lastTabRect = this._lastTabToScrollIntoView.getBoundingClientRect();
+              let selectedTab = this.selectedItem;
+              if (selectedTab.pinned) {
+                selectedTab = null;
+              } else {
+                selectedTab = selectedTab.getBoundingClientRect();
+                selectedTab = {left: selectedTab.left, right: selectedTab.right};
+              }
+              return [
+                this._lastTabToScrollIntoView,
+                this.arrowScrollbox.scrollClientRect,
+                {left: lastTabRect.left, right: lastTabRect.right},
+                selectedTab,
+              ];
+            }).then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
+              // First off, remove the promise so we can re-enter if necessary.
+              delete this._backgroundTabScrollPromise;
+              // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
+              // the code above to get layout info for *that* tab, and don't do
+              // anything here, as we really just want to run this for the last-opened tab.
+              if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
+                this._notifyBackgroundTab(this._lastTabToScrollIntoView);
+                return;
+              }
+              delete this._lastTabToScrollIntoView;
+              // Is the new tab already completely visible?
+              if (scrollRect.left <= tabRect.left && tabRect.right <= scrollRect.right)
+                return;
+
+              if (this.arrowScrollbox.smoothScroll) {
+                // Can we make both the new tab and the selected tab completely visible?
+                if (!selectedRect ||
+                    Math.max(tabRect.right - selectedRect.left, selectedRect.right - tabRect.left) <=
+                      scrollRect.width) {
+                  this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
+                  return;
+                }
+
+                this.arrowScrollbox.scrollByPixels(RTL_UI ?
+                                                     selectedRect.right - scrollRect.right :
+                                                     selectedRect.left - scrollRect.left);
+              }
+
+              if (!this._animateElement.hasAttribute("highlight")) {
+                this._animateElement.setAttribute("highlight", "true");
+                setTimeout(function(ele) {
+                  ele.removeAttribute("highlight");
+                }, 150, this._animateElement);
+              }
+            });
+          }
+        ]]></body>
+      </method>
 
-    event.preventDefault();
-  }
+      <method name="_getDragTargetTab">
+        <parameter name="event"/>
+        <parameter name="isLink"/>
+        <body><![CDATA[
+          let tab = event.target;
+          while (tab && tab.localName != "tab") {
+            tab = tab.parentNode;
+          }
+          if (tab && isLink) {
+            let {width} = tab.getBoundingClientRect();
+            if (event.screenX < tab.screenX + width * .25 ||
+                event.screenX > tab.screenX + width * .75)
+              return null;
+          }
+          return tab;
+        ]]></body>
+      </method>
+
+      <method name="_getDropIndex">
+        <parameter name="event"/>
+        <parameter name="isLink"/>
+        <body><![CDATA[
+          var tabs = this.children;
+          var tab = this._getDragTargetTab(event, isLink);
+          if (!RTL_UI) {
+            for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+              if (event.screenX < tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2)
+                return i;
+          } else {
+            for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+              if (event.screenX > tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2)
+                return i;
+          }
+          return tabs.length;
+        ]]></body>
+      </method>
+
+      <method name="_getDropEffectForTabDrag">
+        <parameter name="event"/>
+        <body><![CDATA[
+          var dt = event.dataTransfer;
+
+          let isMovingTabs = dt.mozItemCount > 0;
+          for (let i = 0; i < dt.mozItemCount; i++) {
+            // tabs are always added as the first type
+            let types = dt.mozTypesAt(0);
+            if (types[0] != TAB_DROP_TYPE) {
+              isMovingTabs = false;
+              break;
+            }
+          }
+
+          if (isMovingTabs) {
+            let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+            if (sourceNode instanceof XULElement &&
+                sourceNode.localName == "tab" &&
+                sourceNode.ownerGlobal.isChromeWindow &&
+                sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
+                sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.parentNode) {
+              // Do not allow transfering a private tab to a non-private window
+              // and vice versa.
+              if (PrivateBrowsingUtils.isWindowPrivate(window) !=
+                  PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal))
+                return "none";
+
+              if (window.gMultiProcessBrowser !=
+                  sourceNode.ownerGlobal.gMultiProcessBrowser)
+                return "none";
+
+              return dt.dropEffect == "copy" ? "copy" : "move";
+            }
+          }
+
+          if (browserDragAndDrop.canDropLink(event)) {
+            return "link";
+          }
+          return "none";
+        ]]></body>
+      </method>
+
+      <method name="_handleNewTab">
+        <parameter name="tab"/>
+        <body><![CDATA[
+          if (tab.parentNode != this) {
+            return;
+          }
+          tab._fullyOpen = true;
+          gBrowser.tabAnimationsInProgress--;
+
+          this._updateCloseButtons();
 
-  on_click(event) {
-    if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
-      /* Catches extra clicks meant for the in-tab close button.
-       * Placed here to avoid leaking (a temporary handler added from the
-       * in-tab close button binding would close over the tab and leak it
-       * until the handler itself was removed). (bug 897751)
-       *
-       * The only sequence in which a second click event (i.e. dblclik)
-       * can be dispatched on an in-tab close button is when it is shown
-       * after the first click (i.e. the first click event was dispatched
-       * on the tab). This happens when we show the close button only on
-       * the active tab. (bug 352021)
-       * The only sequence in which a third click event can be dispatched
-       * on an in-tab close button is when the tab was opened with a
-       * double click on the tabbar. (bug 378344)
-       * In both cases, it is most likely that the close button area has
-       * been accidentally clicked, therefore we do not close the tab.
-       *
-       * We don't want to ignore processing of more than one click event,
-       * though, since the user might actually be repeatedly clicking to
-       * close many tabs at once.
-       */
-      let target = event.originalTarget;
-      if (target.classList.contains("tab-close-button")) {
-        // We preemptively set this to allow the closing-multiple-tabs-
-        // in-a-row case.
-        if (this._blockDblClick) {
-          target._ignoredCloseButtonClicks = true;
-        } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
-          target._ignoredCloseButtonClicks = true;
-          event.stopPropagation();
-          return;
-        } else {
-          // Reset the "ignored click" flag
-          target._ignoredCloseButtonClicks = false;
+          if (tab.getAttribute("selected") == "true") {
+            this._handleTabSelect();
+          } else if (!tab.hasAttribute("skipbackgroundnotify")) {
+            this._notifyBackgroundTab(tab);
+          }
+
+          // XXXmano: this is a temporary workaround for bug 345399
+          // We need to manually update the scroll buttons disabled state
+          // if a tab was inserted to the overflow area or removed from it
+          // without any scrolling and when the tabbar has already
+          // overflowed.
+          this.arrowScrollbox._updateScrollButtonsDisabledState();
+
+          // If this browser isn't lazy (indicating it's probably created by
+          // session restore), preload the next about:newtab if we don't
+          // already have a preloaded browser.
+          if (tab.linkedPanel) {
+            NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
+          }
+        ]]></body>
+      </method>
+
+      <method name="_canAdvanceToTab">
+        <parameter name="aTab"/>
+        <body>
+        <![CDATA[
+          return !aTab.closing;
+        ]]>
+        </body>
+      </method>
+
+      <method name="getRelatedElement">
+        <parameter name="aTab"/>
+        <body>
+        <![CDATA[
+          if (!aTab) {
+            return null;
+          }
+
+          // Cannot access gBrowser before it's initialized.
+          if (!gBrowser) {
+            return this.tabbox.tabpanels.firstElementChild;
+          }
+
+          // If the tab's browser is lazy, we need to `_insertBrowser` in order
+          // to have a linkedPanel.  This will also serve to bind the browser
+          // and make it ready to use when the tab is selected.
+          gBrowser._insertBrowser(aTab);
+          return document.getElementById(aTab.linkedPanel);
+        ]]>
+        </body>
+      </method>
+
+      <method name="_updateNewTabVisibility">
+        <body><![CDATA[
+          // Helper functions to help deal with customize mode wrapping some items
+          let wrap = n => n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
+          let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
+
+          // Starting from the tabs element, find the next sibling that:
+          // - isn't hidden; and
+          // - isn't the all-tabs button.
+          // If it's the new tab button, consider the new tab button adjacent to the tabs.
+          // If the new tab button is marked as adjacent and the tabstrip doesn't
+          // overflow, we'll display the 'new tab' button inline in the tabstrip.
+          // In all other cases, the separate new tab button is displayed in its
+          // customized location.
+          let sib = this;
+          do {
+            sib = unwrap(wrap(sib).nextElementSibling);
+          } while (sib && (sib.hidden ||
+                           sib.id == "alltabs-button"));
+
+          const kAttr = "hasadjacentnewtabbutton";
+          if (sib && sib.id == "new-tab-button") {
+            this.setAttribute(kAttr, "true");
+          } else {
+            this.removeAttribute(kAttr);
+          }
+        ]]></body>
+      </method>
+
+      <method name="onWidgetAfterDOMChange">
+        <parameter name="aNode"/>
+        <parameter name="aNextNode"/>
+        <parameter name="aContainer"/>
+        <body><![CDATA[
+          if (aContainer.ownerDocument == document &&
+              aContainer.id == "TabsToolbar-customization-target") {
+            this._updateNewTabVisibility();
+          }
+        ]]></body>
+      </method>
+
+      <method name="onAreaNodeRegistered">
+        <parameter name="aArea"/>
+        <parameter name="aContainer"/>
+        <body><![CDATA[
+          if (aContainer.ownerDocument == document &&
+              aArea == "TabsToolbar") {
+            this._updateNewTabVisibility();
+          }
+        ]]></body>
+      </method>
+
+      <method name="onAreaReset">
+        <parameter name="aArea"/>
+        <parameter name="aContainer"/>
+        <body><![CDATA[
+          this.onAreaNodeRegistered(aArea, aContainer);
+        ]]></body>
+      </method>
+
+      <method name="_hiddenSoundPlayingStatusChanged">
+        <parameter name="tab"/>
+        <parameter name="opts"/>
+        <body><![CDATA[
+          let closed = opts && opts.closed;
+          if (!closed && tab.soundPlaying && tab.hidden) {
+            this._hiddenSoundPlayingTabs.add(tab);
+            this.setAttribute("hiddensoundplaying", "true");
+          } else {
+            this._hiddenSoundPlayingTabs.delete(tab);
+            if (this._hiddenSoundPlayingTabs.size == 0) {
+              this.removeAttribute("hiddensoundplaying");
+            }
+          }
+        ]]></body>
+      </method>
+    </implementation>
+
+    <handlers>
+      <handler event="TabSelect"><![CDATA[
+        this._handleTabSelect();
+      ]]></handler>
+
+      <handler event="TabClose"><![CDATA[
+        this._hiddenSoundPlayingStatusChanged(event.target, {closed: true});
+      ]]></handler>
+
+      <handler event="TabAttrModified"><![CDATA[
+        if (event.detail.changed.includes("soundplaying") && event.target.hidden) {
+          this._hiddenSoundPlayingStatusChanged(event.target);
         }
-      }
+      ]]></handler>
+
+      <handler event="TabHide"><![CDATA[
+        if (event.target.soundPlaying) {
+          this._hiddenSoundPlayingStatusChanged(event.target);
+        }
+      ]]></handler>
 
-      /* Protects from close-tab-button errant doubleclick:
-       * Since we're removing the event target, if the user
-       * double-clicks the button, the dblclick event will be dispatched
-       * with the tabbar as its event target (and explicit/originalTarget),
-       * which treats that as a mouse gesture for opening a new tab.
-       * In this context, we're manually blocking the dblclick event.
-       */
-      if (this._blockDblClick) {
-        if (!("_clickedTabBarOnce" in this)) {
-          this._clickedTabBarOnce = true;
+      <handler event="TabShow"><![CDATA[
+        if (event.target.soundPlaying) {
+          this._hiddenSoundPlayingStatusChanged(event.target);
+        }
+      ]]></handler>
+
+      <handler event="transitionend"><![CDATA[
+        if (event.propertyName != "max-width") {
           return;
         }
-        delete this._clickedTabBarOnce;
-        this._blockDblClick = false;
-      }
-    } else if (event.eventPhase == Event.BUBBLING_PHASE && event.button == 1) {
-      let tab = event.target ? event.target.closest("tab") : null;
-      if (tab) {
-        gBrowser.removeTab(tab, {
-          animate: true,
-          byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
-        });
-      } else if (event.originalTarget.localName == "scrollbox") {
-        // The user middleclicked on the tabstrip. Check whether the click
-        // was dispatched on the open space of it.
-        let visibleTabs = this._getVisibleTabs();
-        let lastTab = visibleTabs[visibleTabs.length - 1];
-        let winUtils = window.windowUtils;
-        let endOfTab = winUtils.getBoundsWithoutFlushing(lastTab)[RTL_UI ? "left" : "right"];
-        if ((!RTL_UI && event.clientX > endOfTab) ||
-            (RTL_UI && event.clientX < endOfTab)) {
-              BrowserOpenTab();
+
+        let tab = event.target.closest("tab");
+        if (!tab) {
+          return;
         }
-      } else {
-        return;
-      }
-
-      event.stopPropagation();
-    }
-  }
 
-  on_keydown(event) {
-    let { altKey, shiftKey } = event;
-    let [accel, nonAccel] = AppConstants.platform == "macosx" ? [event.metaKey, event.ctrlKey] : [event.ctrlKey, event.metaKey];
-
-    let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
-    let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
-
-    if (!keyComboForMove && !keyComboForFocus) {
-      return;
-    }
-
-    // Don't check if the event was already consumed because tab navigation
-    // should work always for better user experience.
-    let { visibleTabs, selectedTab } = gBrowser;
-    let { arrowKeysShouldWrap } = this;
-    let focusedTabIndex = this.ariaFocusedIndex;
-    if (focusedTabIndex == -1) {
-      focusedTabIndex = visibleTabs.indexOf(selectedTab);
-    }
-    let lastFocusedTabIndex = focusedTabIndex;
-    switch (event.keyCode) {
-      case KeyEvent.DOM_VK_UP:
-        if (keyComboForMove) {
-          gBrowser.moveTabBackward();
-        } else {
-          focusedTabIndex--;
-        }
-        break;
-      case KeyEvent.DOM_VK_DOWN:
-        if (keyComboForMove) {
-          gBrowser.moveTabForward();
-        } else {
-          focusedTabIndex++;
+        if (tab.getAttribute("fadein") == "true") {
+          if (tab._fullyOpen) {
+            this._updateCloseButtons();
+          } else {
+            this._handleNewTab(tab);
+          }
+        } else if (tab.closing) {
+          gBrowser._endRemoveTab(tab);
         }
-        break;
-      case KeyEvent.DOM_VK_RIGHT:
-      case KeyEvent.DOM_VK_LEFT:
-        if (keyComboForMove) {
-          gBrowser.moveTabOver(event);
-        } else if ((!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
-                   (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)) {
-                     focusedTabIndex++;
-        } else {
-          focusedTabIndex--;
-        }
-        break;
-      case KeyEvent.DOM_VK_HOME:
-        if (keyComboForMove) {
-          gBrowser.moveTabToStart();
-        } else {
-          focusedTabIndex = 0;
-        }
-        break;
-      case KeyEvent.DOM_VK_END:
-        if (keyComboForMove) {
-          gBrowser.moveTabToEnd();
-        } else {
-          focusedTabIndex = visibleTabs.length - 1;
-        }
-        break;
-      case KeyEvent.DOM_VK_SPACE:
-        if (visibleTabs[lastFocusedTabIndex].multiselected) {
-          gBrowser.removeFromMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
-        } else {
-          gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex], false);
+
+        let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
+        tab.dispatchEvent(evt);
+      ]]></handler>
+
+      <handler event="dblclick"><![CDATA[
+        // When the tabbar has an unified appearance with the titlebar
+        // and menubar, a double-click in it should have the same behavior
+        // as double-clicking the titlebar
+        if (TabsInTitlebar.enabled)
+          return;
+
+        if (event.button != 0 ||
+            event.originalTarget.localName != "scrollbox")
+          return;
+
+        if (!this._blockDblClick)
+          BrowserOpenTab();
+
+        event.preventDefault();
+      ]]></handler>
+
+      <handler event="click" button="0" phase="capturing"><![CDATA[
+        /* Catches extra clicks meant for the in-tab close button.
+         * Placed here to avoid leaking (a temporary handler added from the
+         * in-tab close button binding would close over the tab and leak it
+         * until the handler itself was removed). (bug 897751)
+         *
+         * The only sequence in which a second click event (i.e. dblclik)
+         * can be dispatched on an in-tab close button is when it is shown
+         * after the first click (i.e. the first click event was dispatched
+         * on the tab). This happens when we show the close button only on
+         * the active tab. (bug 352021)
+         * The only sequence in which a third click event can be dispatched
+         * on an in-tab close button is when the tab was opened with a
+         * double click on the tabbar. (bug 378344)
+         * In both cases, it is most likely that the close button area has
+         * been accidentally clicked, therefore we do not close the tab.
+         *
+         * We don't want to ignore processing of more than one click event,
+         * though, since the user might actually be repeatedly clicking to
+         * close many tabs at once.
+         */
+        let target = event.originalTarget;
+        if (target.classList.contains("tab-close-button")) {
+          // We preemptively set this to allow the closing-multiple-tabs-
+          // in-a-row case.
+          if (this._blockDblClick) {
+            target._ignoredCloseButtonClicks = true;
+          } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
+            target._ignoredCloseButtonClicks = true;
+            event.stopPropagation();
+            return;
+          } else {
+            // Reset the "ignored click" flag
+            target._ignoredCloseButtonClicks = false;
+          }
         }
-        break;
-      default:
-        // Consume the keydown event for the above keyboard
-        // shortcuts only.
-        return;
-    }
 
-    if (arrowKeysShouldWrap) {
-      if (focusedTabIndex >= visibleTabs.length) {
-        focusedTabIndex = 0;
-      } else if (focusedTabIndex < 0) {
-        focusedTabIndex = visibleTabs.length - 1;
-      }
-    } else {
-      focusedTabIndex = Math.min(visibleTabs.length - 1, Math.max(0, focusedTabIndex));
-    }
+        /* Protects from close-tab-button errant doubleclick:
+         * Since we're removing the event target, if the user
+         * double-clicks the button, the dblclick event will be dispatched
+         * with the tabbar as its event target (and explicit/originalTarget),
+         * which treats that as a mouse gesture for opening a new tab.
+         * In this context, we're manually blocking the dblclick event.
+         */
+        if (this._blockDblClick) {
+          if (!("_clickedTabBarOnce" in this)) {
+            this._clickedTabBarOnce = true;
+            return;
+          }
+          delete this._clickedTabBarOnce;
+          this._blockDblClick = false;
+        }
+      ]]></handler>
+
+      <handler event="click"><![CDATA[
+        if (event.button != 1) {
+          return;
+        }
 
-    if (keyComboForFocus &&
-        focusedTabIndex != lastFocusedTabIndex) {
-          this.ariaFocusedItem = visibleTabs[focusedTabIndex];
-    }
-
-    event.preventDefault();
-  }
+        let tab = event.target.closest("tab");
+        if (tab) {
+          gBrowser.removeTab(tab, {
+            animate: true,
+            byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
+          });
+        } else if (event.originalTarget.localName == "scrollbox") {
+          // The user middleclicked on the tabstrip. Check whether the click
+          // was dispatched on the open space of it.
+          let visibleTabs = this._getVisibleTabs();
+          let lastTab = visibleTabs[visibleTabs.length - 1];
+          let winUtils = window.windowUtils;
+          let endOfTab = winUtils.getBoundsWithoutFlushing(lastTab)[RTL_UI ? "left" : "right"];
+          if ((!RTL_UI && event.clientX > endOfTab) ||
+              (RTL_UI && event.clientX < endOfTab)) {
+            BrowserOpenTab();
+          }
+        } else {
+          return;
+        }
 
-  on_dragstart(event) {
-    var tab = this._getDragTargetTab(event, false);
-    if (!tab || this._isCustomizing)
-      return;
+        event.stopPropagation();
+      ]]></handler>
+
+      <handler event="keydown" group="system"><![CDATA[
+        let {altKey, shiftKey} = event;
+        let [accel, nonAccel] = AppConstants.platform == "macosx" ? [event.metaKey, event.ctrlKey] : [event.ctrlKey, event.metaKey];
 
-    let selectedTabs = gBrowser.selectedTabs;
-    let otherSelectedTabs = selectedTabs.filter(selectedTab => selectedTab != tab);
-    let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
+        let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
+        let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
 
-    let dt = event.dataTransfer;
-    for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
-      let dtTab = dataTransferOrderedTabs[i];
+        if (!keyComboForMove && !keyComboForFocus) {
+          return;
+        }
 
-      dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
-      let dtBrowser = dtTab.linkedBrowser;
-
-      // We must not set text/x-moz-url or text/plain data here,
-      // otherwise trying to detach the tab by dropping it on the desktop
-      // may result in an "internet shortcut"
-      dt.mozSetDataAt("text/x-moz-text-internal", dtBrowser.currentURI.spec, i);
-    }
-
-    // Set the cursor to an arrow during tab drags.
-    dt.mozCursor = "default";
-
-    // Set the tab as the source of the drag, which ensures we have a stable
-    // node to deliver the `dragend` event.  See bug 1345473.
-    dt.addElement(tab);
-
-    if (tab.multiselected) {
-      this._groupSelectedTabs(tab);
-    }
+        // Don't check if the event was already consumed because tab navigation
+        // should work always for better user experience.
+        let {visibleTabs, selectedTab} = gBrowser;
+        let {arrowKeysShouldWrap} = this;
+        let focusedTabIndex = this.ariaFocusedIndex;
+        if (focusedTabIndex == -1) {
+          focusedTabIndex = visibleTabs.indexOf(selectedTab);
+        }
+        let lastFocusedTabIndex = focusedTabIndex;
+        switch (event.keyCode) {
+          case KeyEvent.DOM_VK_UP:
+            if (keyComboForMove) {
+              gBrowser.moveTabBackward();
+            } else {
+              focusedTabIndex--;
+            }
+            break;
+          case KeyEvent.DOM_VK_DOWN:
+            if (keyComboForMove) {
+              gBrowser.moveTabForward();
+            } else {
+              focusedTabIndex++;
+            }
+            break;
+          case KeyEvent.DOM_VK_RIGHT:
+          case KeyEvent.DOM_VK_LEFT:
+            if (keyComboForMove) {
+              gBrowser.moveTabOver(event);
+            } else if ((!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
+                       (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)) {
+              focusedTabIndex++;
+            } else {
+              focusedTabIndex--;
+            }
+            break;
+          case KeyEvent.DOM_VK_HOME:
+            if (keyComboForMove) {
+              gBrowser.moveTabToStart();
+            } else {
+              focusedTabIndex = 0;
+            }
+            break;
+          case KeyEvent.DOM_VK_END:
+            if (keyComboForMove) {
+              gBrowser.moveTabToEnd();
+            } else {
+              focusedTabIndex = visibleTabs.length - 1;
+            }
+            break;
+          case KeyEvent.DOM_VK_SPACE:
+            if (visibleTabs[lastFocusedTabIndex].multiselected) {
+              gBrowser.removeFromMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
+            } else {
+              gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex], false);
+            }
+            break;
+          default:
+            // Consume the keydown event for the above keyboard
+            // shortcuts only.
+            return;
+        }
 
-    // Create a canvas to which we capture the current tab.
-    // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
-    // canvas size (in CSS pixels) to the window's backing resolution in order
-    // to get a full-resolution drag image for use on HiDPI displays.
-    let windowUtils = window.windowUtils;
-    let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
-    let canvas = this._dndCanvas;
-    if (!canvas) {
-      this._dndCanvas = canvas =
-      document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-      canvas.style.width = "100%";
-      canvas.style.height = "100%";
-      canvas.mozOpaque = true;
-    }
+        if (arrowKeysShouldWrap) {
+          if (focusedTabIndex >= visibleTabs.length) {
+            focusedTabIndex = 0;
+          } else if (focusedTabIndex < 0) {
+            focusedTabIndex = visibleTabs.length - 1;
+          }
+        } else {
+          focusedTabIndex = Math.min(visibleTabs.length - 1, Math.max(0, focusedTabIndex));
+        }
+
+        if (keyComboForFocus &&
+            focusedTabIndex != lastFocusedTabIndex) {
+          this.ariaFocusedItem = visibleTabs[focusedTabIndex];
+        }
+
+        event.preventDefault();
+      ]]></handler>
+
+      <handler event="dragstart"><![CDATA[
+        var tab = this._getDragTargetTab(event, false);
+        if (!tab || this._isCustomizing)
+          return;
 
-    canvas.width = 160 * scale;
-    canvas.height = 90 * scale;
-    let toDrag = canvas;
-    let dragImageOffset = -16;
-    let browser = tab.linkedBrowser;
-    if (gMultiProcessBrowser) {
-      var context = canvas.getContext("2d");
-      context.fillStyle = "white";
-      context.fillRect(0, 0, canvas.width, canvas.height);
+        let selectedTabs = gBrowser.selectedTabs;
+        let otherSelectedTabs = selectedTabs.filter(selectedTab => selectedTab != tab);
+        let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
+
+        let dt = event.dataTransfer;
+        for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
+          let dtTab = dataTransferOrderedTabs[i];
+
+          dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
+          let dtBrowser = dtTab.linkedBrowser;
 
-      let captureListener;
-      let platform = AppConstants.platform;
-      // On Windows and Mac we can update the drag image during a drag
-      // using updateDragImage. On Linux, we can use a panel.
-      if (platform == "win" || platform == "macosx") {
-        captureListener = function() {
-          dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
-        };
-      } else {
-        // Create a panel to use it in setDragImage
-        // which will tell xul to render a panel that follows
-        // the pointer while a dnd session is on.
-        if (!this._dndPanel) {
-          this._dndCanvas = canvas;
-          this._dndPanel = document.createXULElement("panel");
-          this._dndPanel.className = "dragfeedback-tab";
-          this._dndPanel.setAttribute("type", "drag");
-          let wrapper = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
-          wrapper.style.width = "160px";
-          wrapper.style.height = "90px";
-          wrapper.appendChild(canvas);
-          this._dndPanel.appendChild(wrapper);
-          document.documentElement.appendChild(this._dndPanel);
+          // We must not set text/x-moz-url or text/plain data here,
+          // otherwise trying to detach the tab by dropping it on the desktop
+          // may result in an "internet shortcut"
+          dt.mozSetDataAt("text/x-moz-text-internal", dtBrowser.currentURI.spec, i);
+        }
+
+        // Set the cursor to an arrow during tab drags.
+        dt.mozCursor = "default";
+
+        // Set the tab as the source of the drag, which ensures we have a stable
+        // node to deliver the `dragend` event.  See bug 1345473.
+        dt.addElement(tab);
+
+        if (tab.multiselected) {
+          this._groupSelectedTabs(tab);
         }
-        toDrag = this._dndPanel;
-      }
-      // PageThumb is async with e10s but that's fine
-      // since we can update the image during the dnd.
-      PageThumbs.captureToCanvas(browser, canvas, captureListener);
-    } else {
-      // For the non e10s case we can just use PageThumbs
-      // sync, so let's use the canvas for setDragImage.
-      PageThumbs.captureToCanvas(browser, canvas);
-      dragImageOffset = dragImageOffset * scale;
-    }
-    dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
 
-    // _dragData.offsetX/Y give the coordinates that the mouse should be
-    // positioned relative to the corner of the new window created upon
-    // dragend such that the mouse appears to have the same position
-    // relative to the corner of the dragged tab.
-    function clientX(ele) {
-      return ele.getBoundingClientRect().left;
-    }
-    let tabOffsetX = clientX(tab) - clientX(this);
-    tab._dragData = {
-      offsetX: event.screenX - window.screenX - tabOffsetX,
-      offsetY: event.screenY - window.screenY,
-      scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
-      screenX: event.screenX,
-      movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab])
-                      .filter(t => t.pinned == tab.pinned),
-    };
+        // Create a canvas to which we capture the current tab.
+        // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
+        // canvas size (in CSS pixels) to the window's backing resolution in order
+        // to get a full-resolution drag image for use on HiDPI displays.
+        let windowUtils = window.windowUtils;
+        let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
+        let canvas = this._dndCanvas;
+        if (!canvas) {
+          this._dndCanvas = canvas =
+            document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+          canvas.style.width = "100%";
+          canvas.style.height = "100%";
+          canvas.mozOpaque = true;
+        }
 
-    event.stopPropagation();
-  }
+        canvas.width = 160 * scale;
+        canvas.height = 90 * scale;
+        let toDrag = canvas;
+        let dragImageOffset = -16;
+        let browser = tab.linkedBrowser;
+        if (gMultiProcessBrowser) {
+          var context = canvas.getContext("2d");
+          context.fillStyle = "white";
+          context.fillRect(0, 0, canvas.width, canvas.height);
 
-  on_dragover(event) {
-    var effects = this._getDropEffectForTabDrag(event);
-
-    var ind = this._tabDropIndicator;
-    if (effects == "" || effects == "none") {
-      ind.hidden = true;
-      return;
-    }
-    event.preventDefault();
-    event.stopPropagation();
-
-    var arrowScrollbox = this.arrowScrollbox;
+          let captureListener;
+          let platform = AppConstants.platform;
+          // On Windows and Mac we can update the drag image during a drag
+          // using updateDragImage. On Linux, we can use a panel.
+          if (platform == "win" || platform == "macosx") {
+            captureListener = function() {
+              dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
+            };
+          } else {
+            // Create a panel to use it in setDragImage
+            // which will tell xul to render a panel that follows
+            // the pointer while a dnd session is on.
+            if (!this._dndPanel) {
+              this._dndCanvas = canvas;
+              this._dndPanel = document.createXULElement("panel");
+              this._dndPanel.className = "dragfeedback-tab";
+              this._dndPanel.setAttribute("type", "drag");
+              let wrapper = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+              wrapper.style.width = "160px";
+              wrapper.style.height = "90px";
+              wrapper.appendChild(canvas);
+              this._dndPanel.appendChild(wrapper);
+              document.documentElement.appendChild(this._dndPanel);
+            }
+            toDrag = this._dndPanel;
+          }
+          // PageThumb is async with e10s but that's fine
+          // since we can update the image during the dnd.
+          PageThumbs.captureToCanvas(browser, canvas, captureListener);
+        } else {
+          // For the non e10s case we can just use PageThumbs
+          // sync, so let's use the canvas for setDragImage.
+          PageThumbs.captureToCanvas(browser, canvas);
+          dragImageOffset = dragImageOffset * scale;
+        }
+        dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
 
-    // autoscroll the tab strip if we drag over the scroll
-    // buttons, even if we aren't dragging a tab, but then
-    // return to avoid drawing the drop indicator
-    var pixelsToScroll = 0;
-    if (this.getAttribute("overflow") == "true") {
-      var targetAnonid = event.originalTarget.getAttribute("anonid");
-      switch (targetAnonid) {
-        case "scrollbutton-up":
-          pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
-          break;
-        case "scrollbutton-down":
-          pixelsToScroll = arrowScrollbox.scrollIncrement;
-          break;
-      }
-      if (pixelsToScroll)
-        arrowScrollbox.scrollByPixels((RTL_UI ? -1 : 1) * pixelsToScroll, true);
-    }
+        // _dragData.offsetX/Y give the coordinates that the mouse should be
+        // positioned relative to the corner of the new window created upon
+        // dragend such that the mouse appears to have the same position
+        // relative to the corner of the dragged tab.
+        function clientX(ele) {
+          return ele.getBoundingClientRect().left;
+        }
+        let tabOffsetX = clientX(tab) - clientX(this);
+        tab._dragData = {
+          offsetX: event.screenX - window.screenX - tabOffsetX,
+          offsetY: event.screenY - window.screenY,
+          scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
+          screenX: event.screenX,
+          movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab])
+                      .filter(t => t.pinned == tab.pinned),
+        };
+
+        event.stopPropagation();
+      ]]></handler>
+
+      <handler event="dragover"><![CDATA[
+        var effects = this._getDropEffectForTabDrag(event);
 
-    let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
-    if ((effects == "move" || effects == "copy") &&
-        this == draggedTab.container) {
-          ind.hidden = true;
+        var ind = this._tabDropIndicator;
+        if (effects == "" || effects == "none") {
+          ind.collapsed = true;
+          return;
+        }
+        event.preventDefault();
+        event.stopPropagation();
+
+        var arrowScrollbox = this.arrowScrollbox;
+
+        // autoscroll the tab strip if we drag over the scroll
+        // buttons, even if we aren't dragging a tab, but then
+        // return to avoid drawing the drop indicator
+        var pixelsToScroll = 0;
+        if (this.getAttribute("overflow") == "true") {
+          var targetAnonid = event.originalTarget.getAttribute("anonid");
+          switch (targetAnonid) {
+            case "scrollbutton-up":
+              pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
+              break;
+            case "scrollbutton-down":
+              pixelsToScroll = arrowScrollbox.scrollIncrement;
+              break;
+          }
+          if (pixelsToScroll)
+            arrowScrollbox.scrollByPixels((RTL_UI ? -1 : 1) * pixelsToScroll, true);
+        }
+
+        let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+        if ((effects == "move" || effects == "copy") &&
+            this == draggedTab.parentNode) {
+          ind.collapsed = true;
 
           if (!this._isGroupTabsAnimationOver()) {
             // Wait for grouping tabs animation to finish
             return;
           }
           this._finishGroupSelectedTabs(draggedTab);
 
           if (effects == "move") {
             this._animateTabMove(event);
             return;
           }
-    }
-
-    this._finishAnimateTabMove();
-
-    if (effects == "link") {
-      let tab = this._getDragTargetTab(event, true);
-      if (tab) {
-        if (!this._dragTime)
-          this._dragTime = Date.now();
-        if (Date.now() >= this._dragTime + this._dragOverDelay)
-          this.selectedItem = tab;
-        ind.hidden = true;
-        return;
-      }
-    }
+        }
 
-    var rect = arrowScrollbox.getBoundingClientRect();
-    var newMargin;
-    if (pixelsToScroll) {
-      // if we are scrolling, put the drop indicator at the edge
-      // so that it doesn't jump while scrolling
-      let scrollRect = arrowScrollbox.scrollClientRect;
-      let minMargin = scrollRect.left - rect.left;
-      let maxMargin = Math.min(minMargin + scrollRect.width,
-                               scrollRect.right);
-      if (RTL_UI) {
-        [minMargin, maxMargin] = [this.clientWidth - maxMargin,
-                                  this.clientWidth - minMargin];
-      }
-      newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
-    } else {
-      let newIndex = this._getDropIndex(event, effects == "link");
-      let children = this.allTabs;
-      if (newIndex == children.length) {
-        let tabRect = children[newIndex - 1].getBoundingClientRect();
-        if (RTL_UI) {
-          newMargin = rect.right - tabRect.left;
-        } else {
-          newMargin = tabRect.right - rect.left;
-        }
-      } else {
-        let tabRect = children[newIndex].getBoundingClientRect();
-        if (RTL_UI) {
-          newMargin = rect.right - tabRect.right;
-        } else {
-          newMargin = tabRect.left - rect.left;
-        }
-      }
-    }
-
-    ind.hidden = false;
-
-    newMargin += ind.clientWidth / 2;
-    if (RTL_UI) {
-      newMargin *= -1;
-    }
-
-    ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
-    ind.style.marginInlineStart = (-ind.clientWidth) + "px";
-  }
-
-  on_drop(event) {
-    var dt = event.dataTransfer;
-    var dropEffect = dt.dropEffect;
-    var draggedTab;
-    let movingTabs;
-    if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
-      draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
-      // not our drop then
-      if (!draggedTab)
-        return;
-      movingTabs = draggedTab._dragData.movingTabs;
-      draggedTab.container._finishGroupSelectedTabs(draggedTab);
-    }
+        this._finishAnimateTabMove();
 
-    this._tabDropIndicator.hidden = true;
-    event.stopPropagation();
-    if (draggedTab && dropEffect == "copy") {
-      // copy the dropped tab (wherever it's from)
-      let newIndex = this._getDropIndex(event, false);
-      let draggedTabCopy;
-      for (let tab of movingTabs) {
-        let newTab = gBrowser.duplicateTab(tab);
-        gBrowser.moveTabTo(newTab, newIndex++);
-        if (tab == draggedTab)
-          draggedTabCopy = newTab;
-      }
-      if (draggedTab.container != this || event.shiftKey) {
-        this.selectedItem = draggedTabCopy;
-      }
-    } else if (draggedTab && draggedTab.container == this) {
-      let oldTranslateX = Math.round(draggedTab._dragData.translateX);
-      let tabWidth = Math.round(draggedTab._dragData.tabWidth);
-      let translateOffset = oldTranslateX % tabWidth;
-      let newTranslateX = oldTranslateX - translateOffset;
-      if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
-        newTranslateX += tabWidth;
-      } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
-        newTranslateX -= tabWidth;
-      }
-
-      let dropIndex = "animDropIndex" in draggedTab._dragData &&
-                      draggedTab._dragData.animDropIndex;
-      let incrementDropIndex = true;
-      if (dropIndex && dropIndex > movingTabs[0]._tPos) {
-        dropIndex--;
-        incrementDropIndex = false;
-      }
-
-      let animate = gBrowser.animationsEnabled;
-      if (oldTranslateX && oldTranslateX != newTranslateX && animate) {
-        for (let tab of movingTabs) {
-          tab.setAttribute("tabdrop-samewindow", "true");
-          tab.style.transform = "translateX(" + newTranslateX + "px)";
-          let onTransitionEnd = transitionendEvent => {
-            if (transitionendEvent.propertyName != "transform" ||
-                transitionendEvent.originalTarget != tab) {
-                  return;
-            }
-            tab.removeEventListener("transitionend", onTransitionEnd);
-
-            tab.removeAttribute("tabdrop-samewindow");
-
-            this._finishAnimateTabMove();
-            if (dropIndex !== false) {
-              gBrowser.moveTabTo(tab, dropIndex);
-              if (incrementDropIndex)
-                dropIndex++;
-            }
-
-            gBrowser.syncThrobberAnimations(tab);
-          };
-          tab.addEventListener("transitionend", onTransitionEnd);
-        }
-      } else {
-        this._finishAnimateTabMove();
-        if (dropIndex !== false) {
-          for (let tab of movingTabs) {
-            gBrowser.moveTabTo(tab, dropIndex);
-            if (incrementDropIndex)
-              dropIndex++;
-          }
-        }
-      }
-    } else if (draggedTab) {
-      let newIndex = this._getDropIndex(event, false);
-      let newTabs = [];
-      for (let tab of movingTabs) {
-        let newTab = gBrowser.adoptTab(tab, newIndex++, tab == draggedTab);
-        newTabs.push(newTab);
-      }
-
-      // Restore tab selection
-      gBrowser.addRangeToMultiSelectedTabs(newTabs[0], newTabs[newTabs.length - 1]);
-    } else {
-      // Pass true to disallow dropping javascript: or data: urls
-      let links;
-      try {
-        links = browserDragAndDrop.dropLinks(event, true);
-      } catch (ex) {}
-
-      if (!links || links.length === 0)
-        return;
-
-      let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
-      if (event.shiftKey)
-        inBackground = !inBackground;
-
-      let targetTab = this._getDragTargetTab(event, true);
-      let userContextId = this.selectedItem.getAttribute("usercontextid");
-      let replace = !!targetTab;
-      let newIndex = this._getDropIndex(event, true);
-      let urls = links.map(link => link.url);
-
-      let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(event);
-
-      (async () => {
-        if (urls.length >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
-          // Sync dialog cannot be used inside drop event handler.
-          let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(urls.length,
-                                                                      window);
-          if (!answer) {
+        if (effects == "link") {
+          let tab = this._getDragTargetTab(event, true);
+          if (tab) {
+            if (!this._dragTime)
+              this._dragTime = Date.now();
+            if (Date.now() >= this._dragTime + this._dragOverDelay)
+              this.selectedItem = tab;
+            ind.collapsed = true;
             return;
           }
         }
 
-        gBrowser.loadTabs(urls, {
-          inBackground,
-          replace,
-          allowThirdPartyFixup: true,
-          targetTab,
-          newIndex,
-          userContextId,
-          triggeringPrincipal,
-        });
-      })();
-    }
-
-    if (draggedTab) {
-      delete draggedTab._dragData;
-    }
-  }
-
-  on_dragend(event) {
-    var dt = event.dataTransfer;
-    var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
-
-    // Prevent this code from running if a tabdrop animation is
-    // running since calling _finishAnimateTabMove would clear
-    // any CSS transition that is running.
-    if (draggedTab.hasAttribute("tabdrop-samewindow"))
-      return;
-
-    this._finishGroupSelectedTabs(draggedTab);
-    this._finishAnimateTabMove();
-
-    if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
-      delete draggedTab._dragData;
-      return;
-    }
-
-    // Disable detach within the browser toolbox
-    var eX = event.screenX;
-    var eY = event.screenY;
-    var wX = window.screenX;
-    // check if the drop point is horizontally within the window
-    if (eX > wX && eX < (wX + window.outerWidth)) {
-      // also avoid detaching if the the tab was dropped too close to
-      // the tabbar (half a tab)
-      let rect = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox);
-      let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
-      if (eY < detachTabThresholdY && eY > window.screenY)
-        return;
-    }
-
-    // screen.availLeft et. al. only check the screen that this window is on,
-    // but we want to look at the screen the tab is being dropped onto.
-    var screen = Cc["@mozilla.org/gfx/screenmanager;1"]
-                             .getService(Ci.nsIScreenManager)
-                             .screenForRect(eX, eY, 1, 1);
-    var fullX = {},
-        fullY = {},
-        fullWidth = {},
-        fullHeight = {};
-    var availX = {},
-        availY = {},
-        availWidth = {},
-        availHeight = {};
-    // get full screen rect and available rect, both in desktop pix
-    screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight);
-    screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
-
-    // scale factor to convert desktop pixels to CSS px
-    var scaleFactor =
-    screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
-    // synchronize CSS-px top-left coordinates with the screen's desktop-px
-    // coordinates, to ensure uniqueness across multiple screens
-    // (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY()
-    // and related methods)
-    availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value;
-    availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value;
-    availWidth.value *= scaleFactor;
-    availHeight.value *= scaleFactor;
-
-    // ensure new window entirely within screen
-    var winWidth = Math.min(window.outerWidth, availWidth.value);
-    var winHeight = Math.min(window.outerHeight, availHeight.value);
-    var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value),
-                        availX.value + availWidth.value - winWidth);
-    var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value),
-                       availY.value + availHeight.value - winHeight);
-
-    delete draggedTab._dragData;
-
-    if (gBrowser.tabs.length == 1) {
-      // resize _before_ move to ensure the window fits the new screen.  if
-      // the window is too large for its screen, the window manager may do
-      // automatic repositioning.
-      window.resizeTo(winWidth, winHeight);
-      window.moveTo(left, top);
-      window.focus();
-    } else {
-      let props = { screenX: left, screenY: top, suppressanimation: 1 };
-      if (AppConstants.platform != "win") {
-        props.outerWidth = winWidth;
-        props.outerHeight = winHeight;
-      }
-      gBrowser.replaceTabsWithWindow(draggedTab, props);
-    }
-    event.stopPropagation();
-  }
-
-  on_dragexit(event) {
-    this._dragTime = 0;
-
-    // This does not work at all (see bug 458613)
-    var target = event.relatedTarget;
-    while (target && target != this)
-      target = target.parentNode;
-    if (target)
-      return;
-
-    this._tabDropIndicator.hidden = true;
-    event.stopPropagation();
-  }
-
-  get tabbox() {
-    return document.getElementById("tabbrowser-tabbox");
-  }
-
-  get newTabButton() {
-    return this.querySelector(".tabs-newtab-button");
-  }
-
-  // Accessor for tabs.  arrowScrollbox has two non-tab elements at the
-  // end, everything else is <tab>s
-  get allTabs() {
-    let children = Array.from(this.arrowScrollbox.children);
-    children.pop();
-    children.pop();
-    return children;
-  }
-
-  appendChild(tab) {
-    return this.insertBefore(tab, null);
-  }
-
-  insertBefore(tab, node) {
-    if (!this.arrowScrollbox) {
-      throw new Error("Shouldn't call this without arrowscrollbox");
-    }
-
-    let {arrowScrollbox} = this;
-    if (node == null) {
-      // we have a toolbarbutton and a space at the end of the scrollbox
-      node = arrowScrollbox.lastChild.previousSibling;
-    }
-    return arrowScrollbox.insertBefore(tab, node);
-  }
-
-  set _tabMinWidth(val) {
-    this.style.setProperty("--tab-min-width", val + "px");
-    return val;
-  }
-
-  set _multiselectEnabled(val) {
-    // Unlike boolean HTML attributes, the value of boolean ARIA attributes actually matters.
-    this.setAttribute("aria-multiselectable", !!val);
-    return val;
-  }
-
-  get _multiselectEnabled() {
-    return this.getAttribute("aria-multiselectable") == "true";
-  }
-
-  get _isCustomizing() {
-    return document.documentElement.getAttribute("customizing") == "true";
-  }
-
-  _initializeArrowScrollbox() {
-    let arrowScrollbox = this.arrowScrollbox;
-    arrowScrollbox.addEventListener("underflow", event => {
-      // Ignore underflow events:
-      // - from nested scrollable elements
-      // - for vertical orientation
-      // - corresponding to an overflow event that we ignored
-      if (event.originalTarget != arrowScrollbox.scrollbox ||
-          event.detail == 0 ||
-          !this.hasAttribute("overflow")) {
-            return;
-      }
-
-      this.removeAttribute("overflow");
-
-      if (this._lastTabClosedByMouse) {
-        this._expandSpacerBy(this._scrollButtonWidth);
-      }
-
-      for (let tab of Array.from(gBrowser._removingTabs)) {
-        gBrowser.removeTab(tab);
-      }
-
-      this._positionPinnedTabs();
-    }, true);
-
-    arrowScrollbox.addEventListener("overflow", event => {
-      // Ignore overflow events:
-      // - from nested scrollable elements
-      // - for vertical orientation
-      if (event.originalTarget != arrowScrollbox.scrollbox ||
-          event.detail == 0) {
-            return;
-      }
-
-      this.setAttribute("overflow", "true");
-      this._positionPinnedTabs();
-      this._handleTabSelect(true);
-    });
-
-    // Override scrollbox.xml method, since our scrollbox's children are
-    // inherited from the scrollbox binding parent (this).
-    arrowScrollbox._getScrollableElements = () => {
-      return this.allTabs.filter(arrowScrollbox._canScrollToElement);
-    };
-    arrowScrollbox._canScrollToElement = tab => {
-      return !tab._pinnedUnscrollable && !tab.hidden;
-    };
-  }
-
-  observe(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "nsPref:changed":
-        // This is has to deal with changes in
-        // privacy.userContext.enabled and
-        // privacy.userContext.longPressBehavior.
-        let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled") &&
-                                !PrivateBrowsingUtils.isWindowPrivate(window);
-
-        // This pref won't change so often, so just recreate the menu.
-        let longPressBehavior = Services.prefs.getIntPref("privacy.userContext.longPressBehavior");
-
-        // If longPressBehavior pref is set to 0 (or any invalid value)
-        // long press menu is disabled.
-        if (containersEnabled && (longPressBehavior <= 0 || longPressBehavior > 2)) {
-          containersEnabled = false;
-        }
-
-        // There are separate "new tab" buttons for when the tab strip
-        // is overflowed and when it is not.  Attach the long click
-        // popup to both of them.
-        const newTab = document.getElementById("new-tab-button");
-        const newTab2 = this.newTabButton;
-
-        for (let parent of [newTab, newTab2]) {
-          if (!parent)
-            continue;
-
-          gClickAndHoldListenersOnElement.remove(parent);
-          parent.removeAttribute("type");
-          if (parent.menupopup) {
-            parent.menupopup.remove();
+        var rect = arrowScrollbox.getBoundingClientRect();
+        var newMargin;
+        if (pixelsToScroll) {
+          // if we are scrolling, put the drop indicator at the edge
+          // so that it doesn't jump while scrolling
+          let scrollRect = arrowScrollbox.scrollClientRect;
+          let minMargin = scrollRect.left - rect.left;
+          let maxMargin = Math.min(minMargin + scrollRect.width,
+                                   scrollRect.right);
+          if (RTL_UI) {
+            [minMargin, maxMargin] = [this.clientWidth - maxMargin,
+                                      this.clientWidth - minMargin];
           }
-
-          if (containersEnabled) {
-            let popup = document.createElementNS(
-              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
-              "menupopup");
-            if (parent.id) {
-              popup.id = "newtab-popup";
+          newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
+        } else {
+          let newIndex = this._getDropIndex(event, effects == "link");
+          if (newIndex == this.children.length) {
+            let tabRect = this.children[newIndex - 1].getBoundingClientRect();
+            if (RTL_UI) {
+              newMargin = rect.right - tabRect.left;
             } else {
-              popup.setAttribute("anonid", "newtab-popup");
+              newMargin = tabRect.right - rect.left;
             }
-            popup.className = "new-tab-popup";
-            popup.setAttribute("position", "after_end");
-            popup.addEventListener("popupshowing", event => {
-              createUserContextMenu(event, {
-                useAccessKeys: false,
-                showDefaultTab: Services.prefs.getIntPref("privacy.userContext.longPressBehavior") == 1,
-              });
-            });
-            parent.prepend(popup);
-
-            // longPressBehavior == 2 means that the menu is shown after X
-            // millisecs. Otherwise, with 1, the menu is open immediatelly.
-            if (longPressBehavior == 2) {
-              gClickAndHoldListenersOnElement.add(parent);
+          } else {
+            let tabRect = this.children[newIndex].getBoundingClientRect();
+            if (RTL_UI) {
+              newMargin = rect.right - tabRect.right;
+            } else {
+              newMargin = tabRect.left - rect.left;
             }
-
-            parent.setAttribute("type", "menu");
           }
         }
 
-        break;
-    }
-  }
+        ind.collapsed = false;
+
+        newMargin += ind.clientWidth / 2;
+        if (RTL_UI) {
+          newMargin *= -1;
+        }
+
+        ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
+        ind.style.marginInlineStart = (-ind.clientWidth) + "px";
+      ]]></handler>
 
-  _getVisibleTabs() {
-    // Cannot access gBrowser before it's initialized.
-    if (!gBrowser) {
-      return this.allTabs[0];
-    }
-
-    return gBrowser.visibleTabs;
-  }
+      <handler event="drop"><![CDATA[
+        var dt = event.dataTransfer;
+        var dropEffect = dt.dropEffect;
+        var draggedTab;
+        let movingTabs;
+        if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
+          draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+          // not our drop then
+          if (!draggedTab)
+            return;
+          movingTabs = draggedTab._dragData.movingTabs;
+          draggedTab.parentNode._finishGroupSelectedTabs(draggedTab);
+        }
 
-  _setPositionalAttributes() {
-    let visibleTabs = this._getVisibleTabs();
-    if (!visibleTabs.length) {
-      return;
-    }
-    let selectedTab = this.selectedItem;
-    let selectedIndex = visibleTabs.indexOf(selectedTab);
-    if (this._beforeSelectedTab) {
-      this._beforeSelectedTab.removeAttribute("beforeselected-visible");
-    }
+        this._tabDropIndicator.collapsed = true;
+        event.stopPropagation();
+        if (draggedTab && dropEffect == "copy") {
+          // copy the dropped tab (wherever it's from)
+          let newIndex = this._getDropIndex(event, false);
+          let draggedTabCopy;
+          for (let tab of movingTabs) {
+            let newTab = gBrowser.duplicateTab(tab);
+            gBrowser.moveTabTo(newTab, newIndex++);
+            if (tab == draggedTab)
+              draggedTabCopy = newTab;
+          }
+          if (draggedTab.parentNode != this || event.shiftKey) {
+            this.selectedItem = draggedTabCopy;
+          }
+        } else if (draggedTab && draggedTab.parentNode == this) {
+          let oldTranslateX = Math.round(draggedTab._dragData.translateX);
+          let tabWidth = Math.round(draggedTab._dragData.tabWidth);
+          let translateOffset = oldTranslateX % tabWidth;
+          let newTranslateX = oldTranslateX - translateOffset;
+          if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
+            newTranslateX += tabWidth;
+          } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
+            newTranslateX -= tabWidth;
+          }
 
-    if (selectedTab.closing || selectedIndex <= 0) {
-      this._beforeSelectedTab = null;
-    } else {
-      let beforeSelectedTab = visibleTabs[selectedIndex - 1];
-      let separatedByScrollButton = this.getAttribute("overflow") == "true" &&
-                                    beforeSelectedTab.pinned && !selectedTab.pinned;
-      if (!separatedByScrollButton) {
-        this._beforeSelectedTab = beforeSelectedTab;
-        this._beforeSelectedTab.setAttribute("beforeselected-visible",
-                                             "true");
-      }
-    }
+          let dropIndex = "animDropIndex" in draggedTab._dragData &&
+                          draggedTab._dragData.animDropIndex;
+          let incrementDropIndex = true;
+          if (dropIndex && dropIndex > movingTabs[0]._tPos) {
+            dropIndex--;
+            incrementDropIndex = false;
+          }
+
+          let animate = gBrowser.animationsEnabled;
+          if (oldTranslateX && oldTranslateX != newTranslateX && animate) {
+            for (let tab of movingTabs) {
+              tab.setAttribute("tabdrop-samewindow", "true");
+              tab.style.transform = "translateX(" + newTranslateX + "px)";
+              let onTransitionEnd = transitionendEvent => {
+                if (transitionendEvent.propertyName != "transform" ||
+                    transitionendEvent.originalTarget != tab) {
+                  return;
+                }
+                tab.removeEventListener("transitionend", onTransitionEnd);
+
+                tab.removeAttribute("tabdrop-samewindow");
+
+                this._finishAnimateTabMove();
+                if (dropIndex !== false) {
+                  gBrowser.moveTabTo(tab, dropIndex);
+                  if (incrementDropIndex)
+                    dropIndex++;
+                }
 
-    if (this._firstTab)
-      this._firstTab.removeAttribute("first-visible-tab");
-    this._firstTab = visibleTabs[0];
-    this._firstTab.setAttribute("first-visible-tab", "true");
-    if (this._lastTab)
-      this._lastTab.removeAttribute("last-visible-tab");
-    this._lastTab = visibleTabs[visibleTabs.length - 1];
-    this._lastTab.setAttribute("last-visible-tab", "true");
+                gBrowser.syncThrobberAnimations(tab);
+              };
+              tab.addEventListener("transitionend", onTransitionEnd);
+            }
+          } else {
+            this._finishAnimateTabMove();
+            if (dropIndex !== false) {
+              for (let tab of movingTabs) {
+                gBrowser.moveTabTo(tab, dropIndex);
+                if (incrementDropIndex)
+                  dropIndex++;
+              }
+            }
+          }
+        } else if (draggedTab) {
+          let newIndex = this._getDropIndex(event, false);
+          let newTabs = [];
+          for (let tab of movingTabs) {
+            let newTab = gBrowser.adoptTab(tab, newIndex++, tab == draggedTab);
+            newTabs.push(newTab);
+          }
 
-    let hoveredTab = this._hoveredTab;
-    if (hoveredTab) {
-      hoveredTab._mouseleave();
-    }
-    hoveredTab = this.querySelector("tab:hover");
-    if (hoveredTab) {
-      hoveredTab._mouseenter();
-    }
+          // Restore tab selection
+          gBrowser.addRangeToMultiSelectedTabs(newTabs[0], newTabs[newTabs.length - 1]);
+        } else {
+          // Pass true to disallow dropping javascript: or data: urls
+          let links;
+          try {
+            links = browserDragAndDrop.dropLinks(event, true);
+          } catch (ex) {}
+
+          if (!links || links.length === 0)
+            return;
+
+          let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+          if (event.shiftKey)
+            inBackground = !inBackground;
+
+          let targetTab = this._getDragTargetTab(event, true);
+          let userContextId = this.selectedItem.getAttribute("usercontextid");
+          let replace = !!targetTab;
+          let newIndex = this._getDropIndex(event, true);
+          let urls = links.map(link => link.url);
 
-    // Update before-multiselected attributes.
-    // gBrowser may not be initialized yet, so avoid using it
-    for (let i = 0; i < visibleTabs.length - 1; i++) {
-      let tab = visibleTabs[i];
-      let nextTab = visibleTabs[i + 1];
-      tab.removeAttribute("before-multiselected");
-      if (nextTab.multiselected) {
-        tab.setAttribute("before-multiselected", "true");
-      }
-    }
-  }
+          let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(event);
+
+          (async () => {
+            if (urls.length >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
+              // Sync dialog cannot be used inside drop event handler.
+              let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(urls.length,
+                                                                          window);
+              if (!answer) {
+                return;
+              }
+            }
 
-  _updateCloseButtons() {
-    // If we're overflowing, tabs are at their minimum widths.
-    if (this.getAttribute("overflow") == "true") {
-      this.setAttribute("closebuttons", "activetab");
-      return;
-    }
+            gBrowser.loadTabs(urls, {
+              inBackground,
+              replace,
+              allowThirdPartyFixup: true,
+              targetTab,
+              newIndex,
+              userContextId,
+              triggeringPrincipal,
+            });
+          })();
+        }
 
-    if (this._closeButtonsUpdatePending) {
-      return;
-    }
-    this._closeButtonsUpdatePending = true;
+        if (draggedTab) {
+          delete draggedTab._dragData;
+        }
+      ]]></handler>
+
+      <handler event="dragend"><![CDATA[
+        var dt = event.dataTransfer;
+        var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
 
-    // Wait until after the next paint to get current layout data from
-    // getBoundsWithoutFlushing.
-    window.requestAnimationFrame(() => {
-      window.requestAnimationFrame(() => {
-        this._closeButtonsUpdatePending = false;
+        // Prevent this code from running if a tabdrop animation is
+        // running since calling _finishAnimateTabMove would clear
+        // any CSS transition that is running.
+        if (draggedTab.hasAttribute("tabdrop-samewindow"))
+          return;
 
-        // The scrollbox may have started overflowing since we checked
-        // overflow earlier, so check again.
-        if (this.getAttribute("overflow") == "true") {
-          this.setAttribute("closebuttons", "activetab");
+        this._finishGroupSelectedTabs(draggedTab);
+        this._finishAnimateTabMove();
+
+        if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
+          delete draggedTab._dragData;
           return;
         }
 
-        // Check if tab widths are below the threshold where we want to
-        // remove close buttons from background tabs so that people don't
-        // accidentally close tabs by selecting them.
-        let rect = ele => {
-          return window.windowUtils.getBoundsWithoutFlushing(ele);
-        };
-        let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
-        if (tab && rect(tab).width <= this._tabClipWidth) {
-          this.setAttribute("closebuttons", "activetab");
-        } else {
-          this.removeAttribute("closebuttons");
-        }
-      });
-    });
-  }
-
-  _updateHiddenTabsStatus() {
-    if (gBrowser.visibleTabs.length < gBrowser.tabs.length) {
-      this.setAttribute("hashiddentabs", "true");
-    } else {
-      this.removeAttribute("hashiddentabs");
-    }
-  }
-
-  _handleTabSelect(aInstant) {
-    let selectedTab = this.selectedItem;
-    if (this.getAttribute("overflow") == "true")
-      this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
-
-    selectedTab._notselectedsinceload = false;
-  }
-
-  /**
-   * Try to keep the active tab's close button under the mouse cursor
-   */
-  _lockTabSizing(aTab, aTabWidth) {
-    let tabs = this._getVisibleTabs();
-    if (!tabs.length) {
-      return;
-    }
-
-    var isEndTab = (aTab._tPos > tabs[tabs.length - 1]._tPos);
-
-    if (!this._tabDefaultMaxWidth) {
-      this._tabDefaultMaxWidth =
-      parseFloat(window.getComputedStyle(aTab).maxWidth);
-    }
-    this._lastTabClosedByMouse = true;
-    this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox._scrollButtonDown).width;
-
-    if (this.getAttribute("overflow") == "true") {
-      // Don't need to do anything if we're in overflow mode and aren't scrolled
-      // all the way to the right, or if we're closing the last tab.
-      if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
-        return;
-      }
-      // If the tab has an owner that will become the active tab, the owner will
-      // be to the left of it, so we actually want the left tab to slide over.
-      // This can't be done as easily in non-overflow mode, so we don't bother.
-      if (aTab.owner) {
-        return;
-      }
-      this._expandSpacerBy(aTabWidth);
-    } else { // non-overflow mode
-      // Locking is neither in effect nor needed, so let tabs expand normally.
-      if (isEndTab && !this._hasTabTempMaxWidth) {
-        return;
-      }
-      let numPinned = gBrowser._numPinnedTabs;
-      // Force tabs to stay the same width, unless we're closing the last tab,
-      // which case we need to let them expand just enough so that the overall
-      // tabbar width is the same.
-      if (isEndTab) {
-        let numNormalTabs = tabs.length - numPinned;
-        aTabWidth = aTabWidth * (numNormalTabs + 1) / numNormalTabs;
-        if (aTabWidth > this._tabDefaultMaxWidth) {
-          aTabWidth = this._tabDefaultMaxWidth;
-        }
-      }
-      aTabWidth += "px";
-      let tabsToReset = [];
-      for (let i = numPinned; i < tabs.length; i++) {
-        let tab = tabs[i];
-        tab.style.setProperty("max-width", aTabWidth, "important");
-        if (!isEndTab) { // keep tabs the same width
-          tab.style.transition = "none";
-          tabsToReset.push(tab);
-        }
-      }
-
-      if (tabsToReset.length) {
-        window.promiseDocumentFlushed(() => {}).then(() => {
-          window.requestAnimationFrame(() => {
-            for (let tab of tabsToReset) {
-              tab.style.transition = "";
-            }
-          });
-        });
-      }
-
-      this._hasTabTempMaxWidth = true;
-      gBrowser.addEventListener("mousemove", this);
-      window.addEventListener("mouseout", this);
-    }
-  }
-
-  _expandSpacerBy(pixels) {
-    let spacer = this._closingTabsSpacer;
-    spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
-    this.setAttribute("using-closing-tabs-spacer", "true");
-    gBrowser.addEventListener("mousemove", this);
-    window.addEventListener("mouseout", this);
-  }
-
-  _unlockTabSizing() {
-    gBrowser.removeEventListener("mousemove", this);
-    window.removeEventListener("mouseout", this);
-
-    if (this._hasTabTempMaxWidth) {
-      this._hasTabTempMaxWidth = false;
-      let tabs = this._getVisibleTabs();
-      for (let i = 0; i < tabs.length; i++) {
-        tabs[i].style.maxWidth = "";
-      }
-    }
-
-    if (this.hasAttribute("using-closing-tabs-spacer")) {
-      this.removeAttribute("using-closing-tabs-spacer");
-      this._closingTabsSpacer.style.width = 0;
-    }
-  }
-
-  uiDensityChanged() {
-    this._positionPinnedTabs();
-    this._updateCloseButtons();
-    this._handleTabSelect(true);
-  }
-
-  _positionPinnedTabs() {
-    let numPinned = gBrowser._numPinnedTabs;
-    let doPosition = this.getAttribute("overflow") == "true" &&
-                     this._getVisibleTabs().length > numPinned &&
-                     numPinned > 0;
-    let tabs = this.allTabs;
-
-    if (doPosition) {
-      this.setAttribute("positionpinnedtabs", "true");
-
-      let layoutData = this._pinnedTabsLayoutCache;
-      let uiDensity = document.documentElement.getAttribute("uidensity");
-      if (!layoutData ||
-          layoutData.uiDensity != uiDensity) {
-            let arrowScrollbox = this.arrowScrollbox;
-            layoutData = this._pinnedTabsLayoutCache = {
-              uiDensity,
-              pinnedTabWidth: this.allTabs[0].getBoundingClientRect().width,
-              scrollButtonWidth: arrowScrollbox._scrollButtonDown.getBoundingClientRect().width,
-            };
-      }
-
-      let width = 0;
-      for (let i = numPinned - 1; i >= 0; i--) {
-        let tab = tabs[i];
-        width += layoutData.pinnedTabWidth;
-        tab.style.setProperty("margin-inline-start",
-                             -(width + layoutData.scrollButtonWidth) + "px", "important");
-        tab._pinnedUnscrollable = true;
-      }
-      this.style.paddingInlineStart = width + "px";
-    } else {
-      this.removeAttribute("positionpinnedtabs");
-
-      for (let i = 0; i < numPinned; i++) {
-        let tab = tabs[i];
-        tab.style.marginInlineStart = "";
-        tab._pinnedUnscrollable = false;
-      }
-
-      this.style.paddingInlineStart = "";
-    }
-
-    if (this._lastNumPinned != numPinned) {
-      this._lastNumPinned = numPinned;
-      this._handleTabSelect(true);
-    }
-  }
-
-  _animateTabMove(event) {
-    let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
-    let movingTabs = draggedTab._dragData.movingTabs;
-
-    if (this.getAttribute("movingtab") != "true") {
-      this.setAttribute("movingtab", "true");
-      gNavToolbox.setAttribute("movingtab", "true");
-      if (!draggedTab.multiselected)
-        this.selectedItem = draggedTab;
-    }
-
-    if (!("animLastScreenX" in draggedTab._dragData))
-      draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
-
-    let screenX = event.screenX;
-    if (screenX == draggedTab._dragData.animLastScreenX)
-      return;
-
-    // Direction of the mouse movement.
-    let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
-
-    draggedTab._dragData.animLastScreenX = screenX;
-
-    let pinned = draggedTab.pinned;
-    let numPinned = gBrowser._numPinnedTabs;
-    let tabs = this._getVisibleTabs()
-                   .slice(pinned ? 0 : numPinned,
-                          pinned ? numPinned : undefined);
-
-    if (RTL_UI) {
-      tabs.reverse();
-      // Copy moving tabs array to avoid infinite reversing.
-      movingTabs = [...movingTabs].reverse();
-    }
-    let tabWidth = draggedTab.getBoundingClientRect().width;
-    let shiftWidth = tabWidth * movingTabs.length;
-    draggedTab._dragData.tabWidth = tabWidth;
-
-    // Move the dragged tab based on the mouse position.
-
-    let leftTab = tabs[0];
-    let rightTab = tabs[tabs.length - 1];
-    let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
-    let leftMovingTabScreenX = movingTabs[0].screenX;
-    let translateX = screenX - draggedTab._dragData.screenX;
-    if (!pinned) {
-      translateX += this.arrowScrollbox.scrollbox.scrollLeft - draggedTab._dragData.scrollX;
-    }
-    let leftBound = leftTab.screenX - leftMovingTabScreenX;
-    let rightBound = (rightTab.screenX + rightTab.getBoundingClientRect().width) -
-                     (rightMovingTabScreenX + tabWidth);
-    translateX = Math.min(Math.max(translateX, leftBound), rightBound);
-
-    for (let tab of movingTabs) {
-      tab.style.transform = "translateX(" + translateX + "px)";
-    }
-
-    draggedTab._dragData.translateX = translateX;
-
-    // Determine what tab we're dragging over.
-    // * Single tab dragging: Point of reference is the center of the dragged tab. If that
-    //   point touches a background tab, the dragged tab would take that
-    //   tab's position when dropped.
-    // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
-    //   points of reference (center of tabs on the extremities). When
-    //   mouse is moving from left to right, the right reference gets activated,
-    //   otherwise the left reference will be used. Everything else works the same
-    //   as single tab dragging.
-    // * We're doing a binary search in order to reduce the amount of
-    //   tabs we need to check.
-
-    tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
-    let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
-    let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
-    let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
-    let newIndex = -1;
-    let oldIndex = "animDropIndex" in draggedTab._dragData ?
-                   draggedTab._dragData.animDropIndex : movingTabs[0]._tPos;
-    let low = 0;
-    let high = tabs.length - 1;
-    while (low <= high) {
-      let mid = Math.floor((low + high) / 2);
-      if (tabs[mid] == draggedTab && ++mid > high)
-        break;
-      screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
-      if (screenX > tabCenter) {
-        high = mid - 1;
-      } else if (screenX + tabs[mid].getBoundingClientRect().width < tabCenter) {
-        low = mid + 1;
-      } else {
-        newIndex = tabs[mid]._tPos;
-        break;
-      }
-    }
-    if (newIndex >= oldIndex)
-      newIndex++;
-    if (newIndex < 0 || newIndex == oldIndex)
-      return;
-    draggedTab._dragData.animDropIndex = newIndex;
-
-    // Shift background tabs to leave a gap where the dragged tab
-    // would currently be dropped.
-
-    for (let tab of tabs) {
-      if (tab != draggedTab) {
-        let shift = getTabShift(tab, newIndex);
-        tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
-      }
-    }
-
-    function getTabShift(tab, dropIndex) {
-      if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
-        return (RTL_UI ? -shiftWidth : shiftWidth);
-      if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
-        return (RTL_UI ? shiftWidth : -shiftWidth);
-      return 0;
-    }
-  }
-
-  _finishAnimateTabMove() {
-    if (this.getAttribute("movingtab") != "true") {
-      return;
-    }
-
-    for (let tab of this._getVisibleTabs()) {
-      tab.style.transform = "";
-    }
-
-    this.removeAttribute("movingtab");
-    gNavToolbox.removeAttribute("movingtab");
-
-    this._handleTabSelect();
-  }
-
-  /**
-   * Regroup all selected tabs around the
-   * tab in param
-   */
-  _groupSelectedTabs(tab) {
-    let draggedTabPos = tab._tPos;
-    let selectedTabs = gBrowser.selectedTabs;
-    let animate = gBrowser.animationsEnabled;
-
-    tab.groupingTabsData = {
-      finished: !animate,
-    };
-
-    // Animate left selected tabs
-
-    let insertAtPos = draggedTabPos - 1;
-    for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
-      let movingTab = selectedTabs[i];
-      insertAtPos = newIndex(movingTab, insertAtPos);
-
-      if (animate) {
-        movingTab.groupingTabsData = {};
-        addAnimationData(movingTab, insertAtPos, "left");
-      } else {
-        gBrowser.moveTabTo(movingTab, insertAtPos);
-      }
-      insertAtPos--;
-    }
-
-    // Animate right selected tabs
-
-    insertAtPos = draggedTabPos + 1;
-    for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
-      let movingTab = selectedTabs[i];
-      insertAtPos = newIndex(movingTab, insertAtPos);
-
-      if (animate) {
-        movingTab.groupingTabsData = {};
-        addAnimationData(movingTab, insertAtPos, "right");
-      } else {
-        gBrowser.moveTabTo(movingTab, insertAtPos);
-      }
-      insertAtPos++;
-    }
-
-    // Slide the relevant tabs to their new position.
-    for (let t of this._getVisibleTabs()) {
-      if (t.groupingTabsData && t.groupingTabsData.translateX) {
-        let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
-        t.style.transform = "translateX(" + translateX + "px)";
-      }
-    }
-
-    function newIndex(aTab, index) {
-      // Don't allow mixing pinned and unpinned tabs.
-      if (aTab.pinned) {
-        return Math.min(index, gBrowser._numPinnedTabs - 1);
-      }
-      return Math.max(index, gBrowser._numPinnedTabs);
-    }
-
-    function addAnimationData(movingTab, movingTabNewIndex, side) {
-      let movingTabOldIndex = movingTab._tPos;
-
-      if (movingTabOldIndex == movingTabNewIndex) {
-        // movingTab is already at the right position
-        // and thus don't need to be animated.
-        return;
-      }
-
-      let movingTabWidth = movingTab.getBoundingClientRect().width;
-      let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
-
-      movingTab.groupingTabsData.animate = true;
-      movingTab.setAttribute("tab-grouping", "true");
-
-      movingTab.groupingTabsData.translateX = shift;
-
-      let onTransitionEnd = transitionendEvent => {
-        if (transitionendEvent.propertyName != "transform" ||
-            transitionendEvent.originalTarget != movingTab) {
-              return;
-        }
-        movingTab.removeEventListener("transitionend", onTransitionEnd);
-        movingTab.groupingTabsData.newIndex = movingTabNewIndex;
-        movingTab.groupingTabsData.animate = false;
-      };
-
-      movingTab.addEventListener("transitionend", onTransitionEnd);
-
-      // Add animation data for tabs between movingTab (selected
-      // tab moving towards the dragged tab) and draggedTab.
-      // Those tabs in the middle should move in
-      // the opposite direction of movingTab.
-
-      let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
-      let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
-
-      for (let i = lowerIndex + 1; i < higherIndex; i++) {
-        let middleTab = gBrowser.visibleTabs[i];
-
-        if (middleTab.pinned != movingTab.pinned) {
-          // Don't mix pinned and unpinned tabs
-          break;
+        // Disable detach within the browser toolbox
+        var eX = event.screenX;
+        var eY = event.screenY;
+        var wX = window.screenX;
+        // check if the drop point is horizontally within the window
+        if (eX > wX && eX < (wX + window.outerWidth)) {
+          // also avoid detaching if the the tab was dropped too close to
+          // the tabbar (half a tab)
+          let rect = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox);
+          let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
+          if (eY < detachTabThresholdY && eY > window.screenY)
+            return;
         }
 
-        if (middleTab.multiselected) {
-          // Skip because this selected tab should
-          // be shifted towards the dragged Tab.
-          continue;
-        }
-
-        if (!middleTab.groupingTabsData || !middleTab.groupingTabsData.translateX) {
-          middleTab.groupingTabsData = { translateX: 0 };
-        }
-        if (side == "left") {
-          middleTab.groupingTabsData.translateX -= movingTabWidth;
-        } else {
-          middleTab.groupingTabsData.translateX += movingTabWidth;
-        }
-
-        middleTab.setAttribute("tab-grouping", "true");
-      }
-    }
-  }
-
-  _finishGroupSelectedTabs(tab) {
-    if (!tab.groupingTabsData || tab.groupingTabsData.finished)
-      return;
-
-    tab.groupingTabsData.finished = true;
-
-    let selectedTabs = gBrowser.selectedTabs;
-    let tabIndex = selectedTabs.indexOf(tab);
+        // screen.availLeft et. al. only check the screen that this window is on,
+        // but we want to look at the screen the tab is being dropped onto.
+        var screen = Cc["@mozilla.org/gfx/screenmanager;1"]
+                       .getService(Ci.nsIScreenManager)
+                       .screenForRect(eX, eY, 1, 1);
+        var fullX = {}, fullY = {}, fullWidth = {}, fullHeight = {};
+        var availX = {}, availY = {}, availWidth = {}, availHeight = {};
+        // get full screen rect and available rect, both in desktop pix
+        screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight);
+        screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
 
-    // Moving left tabs
-    for (let i = tabIndex - 1; i > -1; i--) {
-      let movingTab = selectedTabs[i];
-      if (movingTab.groupingTabsData.newIndex) {
-        gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
-      }
-    }
-
-    // Moving right tabs
-    for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
-      let movingTab = selectedTabs[i];
-      if (movingTab.groupingTabsData.newIndex) {
-        gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
-      }
-    }
-
-    for (let t of this._getVisibleTabs()) {
-      t.style.transform = "";
-      t.removeAttribute("tab-grouping");
-      delete t.groupingTabsData;
-    }
-  }
-
-  _isGroupTabsAnimationOver() {
-    for (let tab of gBrowser.selectedTabs) {
-      if (tab.groupingTabsData && tab.groupingTabsData.animate)
-        return false;
-    }
-    return true;
-  }
+        // scale factor to convert desktop pixels to CSS px
+        var scaleFactor =
+          screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
+        // synchronize CSS-px top-left coordinates with the screen's desktop-px
+        // coordinates, to ensure uniqueness across multiple screens
+        // (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY()
+        // and related methods)
+        availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value;
+        availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value;
+        availWidth.value *= scaleFactor;
+        availHeight.value *= scaleFactor;
 
-  handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case "resize":
-        if (aEvent.target != window)
-          break;
+        // ensure new window entirely within screen
+        var winWidth = Math.min(window.outerWidth, availWidth.value);
+        var winHeight = Math.min(window.outerHeight, availHeight.value);
+        var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value),
+                            availX.value + availWidth.value - winWidth);
+        var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value),
+                           availY.value + availHeight.value - winHeight);
 
-        this._updateCloseButtons();
-        this._handleTabSelect(true);
-        break;
-      case "mouseout":
-        // If the "related target" (the node to which the pointer went) is not
-        // a child of the current document, the mouse just left the window.
-        let relatedTarget = aEvent.relatedTarget;
-        if (relatedTarget && relatedTarget.ownerDocument == document)
-          break;
-      case "mousemove":
-        if (document.getElementById("tabContextMenu").state != "open")
-          this._unlockTabSizing();
-        break;
-      default:
-        let methodName = `on_${aEvent.type}`;
-        if (methodName in this) {
-          this[methodName](aEvent);
-        } else {
-          throw new Error(`Unexpected event ${aEvent.type}`);
-        }
-    }
-  }
+        delete draggedTab._dragData;
 
-  _notifyBackgroundTab(aTab) {
-    if (aTab.pinned || aTab.hidden || this.getAttribute("overflow") != "true")
-      return;
-
-    this._lastTabToScrollIntoView = aTab;
-    if (!this._backgroundTabScrollPromise) {
-      this._backgroundTabScrollPromise = window.promiseDocumentFlushed(() => {
-        let lastTabRect = this._lastTabToScrollIntoView.getBoundingClientRect();
-        let selectedTab = this.selectedItem;
-        if (selectedTab.pinned) {
-          selectedTab = null;
+        if (gBrowser.tabs.length == 1) {
+          // resize _before_ move to ensure the window fits the new screen.  if
+          // the window is too large for its screen, the window manager may do
+          // automatic repositioning.
+          window.resizeTo(winWidth, winHeight);
+          window.moveTo(left, top);
+          window.focus();
         } else {
-          selectedTab = selectedTab.getBoundingClientRect();
-          selectedTab = { left: selectedTab.left, right: selectedTab.right };
+          let props = { screenX: left, screenY: top, suppressanimation: 1 };
+          if (AppConstants.platform != "win") {
+            props.outerWidth = winWidth;
+            props.outerHeight = winHeight;
+          }
+          gBrowser.replaceTabsWithWindow(draggedTab, props);
         }
-        return [
-          this._lastTabToScrollIntoView,
-          this.arrowScrollbox.scrollClientRect,
-          { left: lastTabRect.left, right: lastTabRect.right },
-          selectedTab,
-        ];
-      }).then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
-        // First off, remove the promise so we can re-enter if necessary.
-        delete this._backgroundTabScrollPromise;
-        // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
-        // the code above to get layout info for *that* tab, and don't do
-        // anything here, as we really just want to run this for the last-opened tab.
-        if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
-          this._notifyBackgroundTab(this._lastTabToScrollIntoView);
-          return;
-        }
-        delete this._lastTabToScrollIntoView;
-        // Is the new tab already completely visible?
-        if (scrollRect.left <= tabRect.left && tabRect.right <= scrollRect.right)
+        event.stopPropagation();
+      ]]></handler>
+
+      <handler event="dragexit"><![CDATA[
+        this._dragTime = 0;
+
+        // This does not work at all (see bug 458613)
+        var target = event.relatedTarget;
+        while (target && target != this)
+          target = target.parentNode;
+        if (target)
           return;
 
-        if (this.arrowScrollbox.smoothScroll) {
-          // Can we make both the new tab and the selected tab completely visible?
-          if (!selectedRect ||
-              Math.max(tabRect.right - selectedRect.left, selectedRect.right - tabRect.left) <=
-            scrollRect.width) {
-              this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
-              return;
-          }
-
-          this.arrowScrollbox.scrollByPixels(RTL_UI ?
-                                             selectedRect.right - scrollRect.right :
-                                             selectedRect.left - scrollRect.left);
-        }
-
-        if (!this._animateElement.hasAttribute("highlight")) {
-          this._animateElement.setAttribute("highlight", "true");
-          setTimeout(function(ele) {
-            ele.removeAttribute("highlight");
-          }, 150, this._animateElement);
-        }
-      });
-    }
-  }
-
-  _getDragTargetTab(event, isLink) {
-    let tab = event.target;
-    while (tab && tab.localName != "tab") {
-      tab = tab.parentNode;
-    }
-    if (tab && isLink) {
-      let { width } = tab.getBoundingClientRect();
-      if (event.screenX < tab.screenX + width * .25 ||
-          event.screenX > tab.screenX + width * .75)
-        return null;
-    }
-    return tab;
-  }
-
-  _getDropIndex(event, isLink) {
-    var tabs = this.allTabs;
-    var tab = this._getDragTargetTab(event, isLink);
-    if (!RTL_UI) {
-      for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
-        if (event.screenX < tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2)
-        return i;
-    } else {
-      for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
-        if (event.screenX > tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2)
-        return i;
-    }
-    return tabs.length;
-  }
-
-  _getDropEffectForTabDrag(event) {
-    var dt = event.dataTransfer;
-
-    let isMovingTabs = dt.mozItemCount > 0;
-    for (let i = 0; i < dt.mozItemCount; i++) {
-      // tabs are always added as the first type
-      let types = dt.mozTypesAt(0);
-      if (types[0] != TAB_DROP_TYPE) {
-        isMovingTabs = false;
-        break;
-      }
-    }
-
-    if (isMovingTabs) {
-      let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
-      if (sourceNode instanceof XULElement &&
-          sourceNode.localName == "tab" &&
-          sourceNode.ownerGlobal.isChromeWindow &&
-          sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
-          sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container) {
-            // Do not allow transfering a private tab to a non-private window
-            // and vice versa.
-            if (PrivateBrowsingUtils.isWindowPrivate(window) !=
-              PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal))
-              return "none";
-
-            if (window.gMultiProcessBrowser !=
-              sourceNode.ownerGlobal.gMultiProcessBrowser)
-              return "none";
-
-            return dt.dropEffect == "copy" ? "copy" : "move";
-      }
-    }
-
-    if (browserDragAndDrop.canDropLink(event)) {
-      return "link";
-    }
-    return "none";
-  }
-
-  _handleNewTab(tab) {
-    if (tab.container != this) {
-      return;
-    }
-    tab._fullyOpen = true;
-    gBrowser.tabAnimationsInProgress--;
-
-    this._updateCloseButtons();
-
-    if (tab.getAttribute("selected") == "true") {
-      this._handleTabSelect();
-    } else if (!tab.hasAttribute("skipbackgroundnotify")) {
-      this._notifyBackgroundTab(tab);
-    }
-
-    // XXXmano: this is a temporary workaround for bug 345399
-    // We need to manually update the scroll buttons disabled state
-    // if a tab was inserted to the overflow area or removed from it
-    // without any scrolling and when the tabbar has already
-    // overflowed.
-    this.arrowScrollbox._updateScrollButtonsDisabledState();
-
-    // If this browser isn't lazy (indicating it's probably created by
-    // session restore), preload the next about:newtab if we don't
-    // already have a preloaded browser.
-    if (tab.linkedPanel) {
-      NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
-    }
-  }
-
-  _canAdvanceToTab(aTab) {
-    return !aTab.closing;
-  }
-
-  getRelatedElement(aTab) {
-    if (!aTab) {
-      return null;
-    }
-
-    // Cannot access gBrowser before it's initialized.
-    if (!gBrowser._initialized) {
-      return this.tabbox.tabpanels.firstElementChild;
-    }
-
-    // If the tab's browser is lazy, we need to `_insertBrowser` in order
-    // to have a linkedPanel.  This will also serve to bind the browser
-    // and make it ready to use when the tab is selected.
-    gBrowser._insertBrowser(aTab);
-    return document.getElementById(aTab.linkedPanel);
-  }
-
-  _updateNewTabVisibility() {
-    // Helper functions to help deal with customize mode wrapping some items
-    let wrap = n => n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
-    let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
-
-    // Starting from the tabs element, find the next sibling that:
-    // - isn't hidden; and
-    // - isn't the all-tabs button.
-    // If it's the new tab button, consider the new tab button adjacent to the tabs.
-    // If the new tab button is marked as adjacent and the tabstrip doesn't
-    // overflow, we'll display the 'new tab' button inline in the tabstrip.
-    // In all other cases, the separate new tab button is displayed in its
-    // customized location.
-    let sib = this;
-    do {
-      sib = unwrap(wrap(sib).nextElementSibling);
-    } while (sib && (sib.hidden ||
-                     sib.id == "alltabs-button"));
-
-    const kAttr = "hasadjacentnewtabbutton";
-    if (sib && sib.id == "new-tab-button") {
-      this.setAttribute(kAttr, "true");
-    } else {
-      this.removeAttribute(kAttr);
-    }
-  }
-
-  onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
-    if (aContainer.ownerDocument == document &&
-        aContainer.id == "TabsToolbar-customization-target") {
-          this._updateNewTabVisibility();
-    }
-  }
-
-  onAreaNodeRegistered(aArea, aContainer) {
-    if (aContainer.ownerDocument == document &&
-        aArea == "TabsToolbar") {
-          this._updateNewTabVisibility();
-    }
-  }
-
-  onAreaReset(aArea, aContainer) {
-    this.onAreaNodeRegistered(aArea, aContainer);
-  }
-
-  _hiddenSoundPlayingStatusChanged(tab, opts) {
-    let closed = opts && opts.closed;
-    if (!closed && tab.soundPlaying && tab.hidden) {
-      this._hiddenSoundPlayingTabs.add(tab);
-      this.setAttribute("hiddensoundplaying", "true");
-    } else {
-      this._hiddenSoundPlayingTabs.delete(tab);
-      if (this._hiddenSoundPlayingTabs.size == 0) {
-        this.removeAttribute("hiddensoundplaying");
-      }
-    }
-  }
-
-  destroy() {
-    if (this.boundObserve) {
-      Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
-    }
-
-    CustomizableUI.removeListener(this);
-  }
-}
-
-customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {extends: "tabs"});
-}
+        this._tabDropIndicator.collapsed = true;
+        event.stopPropagation();
+      ]]></handler>
+    </handlers>
+  </binding>
+</bindings>
--- a/browser/base/content/test/general/browser_bug462673.js
+++ b/browser/base/content/test/general/browser_bug462673.js
@@ -1,36 +1,36 @@
 add_task(async function() {
   var win = openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", "chrome,all,dialog=no");
   await SimpleTest.promiseFocus(win);
 
-  let tab = win.gBrowser.tabs[0];
+  let tab = win.gBrowser.tabContainer.firstElementChild;
   await promiseTabLoadEvent(tab, getRootDirectory(gTestPath) + "test_bug462673.html");
 
   is(win.gBrowser.browsers.length, 2, "test_bug462673.html has opened a second tab");
   is(win.gBrowser.selectedTab, tab.nextElementSibling, "dependent tab is selected");
   win.gBrowser.removeTab(tab);
 
   // Closing a tab will also close its parent chrome window, but async
   await BrowserTestUtils.domWindowClosed(win);
 });
 
 add_task(async function() {
   var win = openDialog(AppConstants.BROWSER_CHROME_URL, "_blank", "chrome,all,dialog=no");
   await SimpleTest.promiseFocus(win);
 
-  let tab = win.gBrowser.tabs[0];
+  let tab = win.gBrowser.tabContainer.firstElementChild;
   await promiseTabLoadEvent(tab, getRootDirectory(gTestPath) + "test_bug462673.html");
 
   var newTab = BrowserTestUtils.addTab(win.gBrowser);
   var newBrowser = newTab.linkedBrowser;
   win.gBrowser.removeTab(tab);
   ok(!win.closed, "Window stays open");
   if (!win.closed) {
-    is(win.gBrowser.tabs.length, 1, "Window has one tab");
+    is(win.gBrowser.tabContainer.childElementCount, 1, "Window has one tab");
     is(win.gBrowser.browsers.length, 1, "Window has one browser");
     is(win.gBrowser.selectedTab, newTab, "Remaining tab is selected");
     is(win.gBrowser.selectedBrowser, newBrowser, "Browser for remaining tab is selected");
     is(win.gBrowser.tabbox.selectedPanel, newBrowser.parentNode.parentNode.parentNode, "Panel for remaining tab is selected");
   }
 
   await promiseWindowClosed(win);
 });
--- a/browser/base/content/test/general/browser_ctrlTab.js
+++ b/browser/base/content/test/general/browser_ctrlTab.js
@@ -86,22 +86,22 @@ add_task(async function() {
        "Ctrl+Tab*2 -> Ctrl+W removes the second most recently selected tab");
 
     await pressCtrlTab(true);
     await pressCtrlTab(true);
     await releaseCtrl();
     ok(selectedTab.selected,
        "Ctrl+Tab*2 -> Ctrl+W -> Ctrl+Shift+Tab*2 keeps the selected tab");
   }
-  gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+  gBrowser.removeTab(gBrowser.tabContainer.lastElementChild);
   checkTabs(2);
 
   await ctrlTabTest([1], 1, 0);
 
-  gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+  gBrowser.removeTab(gBrowser.tabContainer.lastElementChild);
   checkTabs(1);
 
   { // test for bug 445768
     let focusedWindow = document.commandDispatcher.focusedWindow;
     let eventConsumed = true;
     let detectKeyEvent = function(event) {
       eventConsumed = event.defaultPrevented;
     };
--- a/browser/base/content/test/general/browser_tabs_owner.js
+++ b/browser/base/content/test/general/browser_tabs_owner.js
@@ -1,31 +1,32 @@
 function test() {
   BrowserTestUtils.addTab(gBrowser);
   BrowserTestUtils.addTab(gBrowser);
   BrowserTestUtils.addTab(gBrowser);
 
+  var tabs = gBrowser.tabs;
   var owner;
 
-  is(gBrowser.tabs.length, 4, "4 tabs are open");
+  is(tabs.length, 4, "4 tabs are open");
 
-  owner = gBrowser.selectedTab = gBrowser.tabs[2];
+  owner = gBrowser.selectedTab = tabs[2];
   BrowserOpenTab();
-  is(gBrowser.selectedTab, gBrowser.tabs[4], "newly opened tab is selected");
+  is(gBrowser.selectedTab, tabs[4], "newly opened tab is selected");
   gBrowser.removeCurrentTab();
   is(gBrowser.selectedTab, owner, "owner is selected");
 
   owner = gBrowser.selectedTab;
   BrowserOpenTab();
-  gBrowser.selectedTab = gBrowser.tabs[1];
-  gBrowser.selectedTab = gBrowser.tabs[4];
+  gBrowser.selectedTab = tabs[1];
+  gBrowser.selectedTab = tabs[4];
   gBrowser.removeCurrentTab();
   isnot(gBrowser.selectedTab, owner, "selecting a different tab clears the owner relation");
 
   owner = gBrowser.selectedTab;
   BrowserOpenTab();
   gBrowser.moveTabTo(gBrowser.selectedTab, 0);
   gBrowser.removeCurrentTab();
-  is(gBrowser.selectedTab, owner, "owner relationship persists when tab is moved");
+  is(gBrowser.selectedTab, owner, "owner relatitionship persists when tab is moved");
 
-  while (gBrowser.tabs.length > 1)
+  while (tabs.length > 1)
     gBrowser.removeCurrentTab();
 }
--- a/browser/base/content/test/performance/browser_startup_images.js
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -27,16 +27,28 @@ const whitelist = [
   },
 
   {
     file: "chrome://browser/skin/places/toolbarDropMarker.png",
     platforms: ["linux", "win", "macosx"],
   },
 
   {
+    file: "chrome://browser/skin/tabbrowser/tabDragIndicator.png",
+    hidpi: "chrome://browser/skin/tabbrowser/tabDragIndicator@2x.png",
+    platforms: ["macosx"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/tabDragIndicator.png",
+    hidpi: "<not loaded>",
+    platforms: ["linux", "win"],
+  },
+
+  {
     file: "resource://gre-resources/loading-image.png",
     platforms: ["win", "macosx"],
     intermittentNotLoaded: ["win", "macosx"],
   },
   {
     file: "resource://gre-resources/broken-image.png",
     platforms: ["win", "macosx"],
     intermittentNotLoaded: ["win", "macosx"],
--- a/browser/base/content/test/performance/browser_tabclose.js
+++ b/browser/base/content/test/performance/browser_tabclose.js
@@ -21,18 +21,20 @@ const EXPECTED_REFLOWS = [
  */
 add_task(async function() {
   await ensureNoPreloadedBrowser();
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
   await BrowserTestUtils.waitForCondition(() => tab._fullyOpen);
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
-  let newTabButtonRect = gBrowser.tabContainer.newTabButton
-                                 .getBoundingClientRect();
+  let newTabButtonRect =
+    document.getAnonymousElementByAttribute(gBrowser.tabContainer,
+                                            "anonid", "tabs-newtab-button")
+            .getBoundingClientRect();
   let inRange = (val, min, max) => min <= val && val <= max;
 
   // Add a reflow observer and open a new tab.
   await withPerfObserver(async function() {
     let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
     gBrowser.removeTab(tab, { animate: true });
     await BrowserTestUtils.waitForEvent(tab, "TabAnimationEnd");
     await switchDone;
--- a/browser/base/content/test/performance/browser_tabdetach.js
+++ b/browser/base/content/test/performance/browser_tabdetach.js
@@ -7,30 +7,28 @@
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
   {
     stack: [
-      "clientX@chrome://browser/content/tabbrowser-tabs.js",
-      "on_dragstart@chrome://browser/content/tabbrowser-tabs.js",
-      "handleEvent@chrome://browser/content/tabbrowser-tabs.js",
+      "clientX@chrome://browser/content/tabbrowser.xml",
+      "onxbldragstart@chrome://browser/content/tabbrowser.xml",
       "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
       "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
       "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
     ],
     maxCount: 2,
   },
 
   {
     stack: [
-      "on_dragstart@chrome://browser/content/tabbrowser-tabs.js",
-      "handleEvent@chrome://browser/content/tabbrowser-tabs.js",
+      "onxbldragstart@chrome://browser/content/tabbrowser.xml",
       "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
       "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
       "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
     ],
   },
 ];
 
 /**
--- a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
+++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
@@ -109,17 +109,17 @@ add_task(async function() {
 
   // First, we'll check that the first tab is actually scrolled
   // at least partially out of view.
   Assert.ok(arrowScrollbox.scrollPosition > 0,
             "First tab should be partially scrolled out of view.");
 
   // Now switch to the first tab. We shouldn't flush layout at all.
   await withPerfObserver(async function() {
-    let firstTab = gBrowser.tabs[0];
+    let firstTab = gBrowser.tabContainer.firstElementChild;
     await BrowserTestUtils.switchTab(gBrowser, firstTab);
     await BrowserTestUtils.waitForCondition(() => {
       return gBrowser.tabContainer.arrowScrollbox.hasAttribute("scrolledtostart");
     });
   }, {expectedReflows: [], frames: ignoreTabstripRects});
 
   // Okay, now close the last tab. The tabstrip should stay overflowed, but removing
   // one more after that should underflow it.
@@ -127,17 +127,17 @@ add_task(async function() {
 
   Assert.ok(gBrowser.tabContainer.hasAttribute("overflow"),
             "Tabs should still be overflowed.");
 
   // Depending on the size of the window, it might take one or more tab
   // removals to put the tab strip out of the overflow state, so we'll just
   // keep testing removals until that occurs.
   while (gBrowser.tabContainer.hasAttribute("overflow")) {
-    lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+    lastTab = gBrowser.tabContainer.lastElementChild;
     if (gBrowser.selectedTab !== lastTab) {
       await BrowserTestUtils.switchTab(gBrowser, lastTab);
     }
 
     // ... and make sure we don't flush layout when closing it, and exiting
     // the overflowed state.
     await withPerfObserver(async function() {
       let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
--- a/browser/base/content/test/performance/head.js
+++ b/browser/base/content/test/performance/head.js
@@ -273,17 +273,19 @@ async function ensureFocusedUrlbar() {
  * tabstrip without causing it to overflow.
  *
  * @return int
  *         The maximum additional tabs that can be fit into the
  *         tabstrip without causing it to overflow.
  */
 function computeMaxTabCount() {
   let currentTabCount = gBrowser.tabs.length;
-  let newTabButton = gBrowser.tabContainer.newTabButton;
+  let newTabButton =
+    document.getAnonymousElementByAttribute(gBrowser.tabContainer,
+                                            "anonid", "tabs-newtab-button");
   let newTabRect = newTabButton.getBoundingClientRect();
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
   let availableTabStripWidth = tabStripRect.width - newTabRect.width;
 
   let tabMinWidth =
     parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth, 10);
 
   let maxTabCount = Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
--- a/browser/base/content/test/tabs/browser_audioTabIcon.js
+++ b/browser/base/content/test/tabs/browser_audioTabIcon.js
@@ -159,17 +159,17 @@ async function test_playing_icon_on_tab(
 }
 
 async function test_playing_icon_on_hidden_tab(tab) {
   let oldSelectedTab = gBrowser.selectedTab;
   let otherTabs = [
     await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
     await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true),
   ];
-  let tabContainer = tab.container;
+  let tabContainer = tab.parentNode;
   let alltabsButton = document.getElementById("alltabs-button");
   let alltabsBadge = alltabsButton.badgeLabel;
 
   function assertIconShowing() {
     is(getComputedStyle(alltabsBadge).backgroundImage,
       'url("chrome://browser/skin/tabbrowser/badge-audio-playing.svg")',
       "The audio playing icon is shown");
     is(tabContainer.getAttribute("hiddensoundplaying"), "true", "There are hidden audio tabs");
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js
@@ -16,17 +16,18 @@ add_task(async function test() {
   await triggerClickOn(tab2, {ctrlKey: true});
 
   ok(tab1.multiselected, "Tab1 is multi-selected");
   ok(tab2.multiselected, "Tab2 is multi-selected");
   ok(!tab3.multiselected, "Tab3 is not multi-selected");
 
   let metaKeyEvent = AppConstants.platform == "macosx" ? {metaKey: true} : {ctrlKey: true};
 
-  let newTabButton = gBrowser.tabContainer.newTabButton;
+  let tabs = document.getElementById("tabbrowser-tabs");
+  let newTabButton = document.getAnonymousElementByAttribute(tabs, "anonid", "tabs-newtab-button");
   let promiseTabOpened = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
   EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent);
   let openEvent = await promiseTabOpened;
   let newTab = openEvent.target;
 
   is(newTab.previousElementSibling, tab2,
     "New tab should be opened after tab2 when tab1 and tab2 are multiselected");
   is(newTab.nextElementSibling, tab3,
@@ -53,55 +54,49 @@ add_task(async function test() {
   ok(tab1.multiselected, "Tab1 is multi-selected");
   ok(!tab2.multiselected, "Tab2 is not multi-selected");
   ok(tab3.multiselected, "Tab3 is multi-selected");
 
   promiseTabOpened = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
   EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent);
   openEvent = await promiseTabOpened;
   newTab = openEvent.target;
-  let previous = gBrowser.tabContainer.findNextTab(newTab, {direction: -1});
-  is(previous, tab3,
-     "New tab should be opened after tab3 when tab1 and tab3 are selected");
-  let next = gBrowser.tabContainer.findNextTab(newTab, {direction: 1});
-  is(next, null,
+  is(newTab.previousElementSibling, tab3,
+    "New tab should be opened after tab3 when tab1 and tab3 are selected");
+  is(newTab.nextElementSibling, null,
     "New tab should be opened at the end of the tabstrip when tab1 and tab3 are selected");
   BrowserTestUtils.removeTab(newTab);
 
   await BrowserTestUtils.switchTab(gBrowser, tab1);
   ok(!tab1.multiselected, "Tab1 is not multi-selected");
   ok(!tab2.multiselected, "Tab2 is not multi-selected");
   ok(!tab3.multiselected, "Tab3 is not multi-selected");
 
   promiseTabOpened = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
   EventUtils.synthesizeMouseAtCenter(newTabButton, {});
   openEvent = await promiseTabOpened;
   newTab = openEvent.target;
-  previous = gBrowser.tabContainer.findNextTab(newTab, {direction: -1});
-  is(previous, tab3,
+  is(newTab.previousElementSibling, tab3,
     "New tab should be opened after tab3 when ctrlKey is not used without multiselection");
-  next = gBrowser.tabContainer.findNextTab(newTab, {direction: 1});
-  is(next, null,
+  is(newTab.nextElementSibling, null,
     "New tab should be opened at the end of the tabstrip when ctrlKey is not used without multiselection");
   BrowserTestUtils.removeTab(newTab);
 
   await BrowserTestUtils.switchTab(gBrowser, tab1);
   await triggerClickOn(tab2, { ctrlKey: true });
   ok(tab1.multiselected, "Tab1 is multi-selected");
   ok(tab2.multiselected, "Tab2 is multi-selected");
   ok(!tab3.multiselected, "Tab3 is not multi-selected");
 
   promiseTabOpened = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
   EventUtils.synthesizeMouseAtCenter(newTabButton, {});
   openEvent = await promiseTabOpened;
   newTab = openEvent.target;
-  previous = gBrowser.tabContainer.findNextTab(newTab, {direction: -1});
-  is(previous, tab3,
+  is(newTab.previousElementSibling, tab3,
     "New tab should be opened after tab3 when ctrlKey is not used with multiselection");
-  next = gBrowser.tabContainer.findNextTab(newTab, {direction: 1});
-  is(next, null,
+  is(newTab.nextElementSibling, null,
     "New tab should be opened at the end of the tabstrip when ctrlKey is not used with multiselection");
   BrowserTestUtils.removeTab(newTab);
 
   BrowserTestUtils.removeTab(tab1);
   BrowserTestUtils.removeTab(tab2);
   BrowserTestUtils.removeTab(tab3);
 });
--- a/browser/base/content/test/tabs/browser_overflowScroll.js
+++ b/browser/base/content/test/tabs/browser_overflowScroll.js
@@ -5,46 +5,47 @@ requestLongerTimeout(2);
 /**
  * Tests that scrolling the tab strip via the scroll buttons scrolls the right
  * amount in non-smoothscroll mode.
  */
 add_task(async function() {
   let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
   let scrollbox = arrowScrollbox.scrollbox;
   let originalSmoothScroll = arrowScrollbox.smoothScroll;
+  let tabs = gBrowser.tabs;
   let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth);
 
   let rect = ele => ele.getBoundingClientRect();
   let width = ele => rect(ele).width;
 
   let tabCountForOverflow = Math.ceil(width(arrowScrollbox) / tabMinWidth * 3);
 
   let left = ele => rect(ele).left;
   let right = ele => rect(ele).right;
   let isLeft = (ele, msg) => is(left(ele), left(scrollbox), msg);
   let isRight = (ele, msg) => is(right(ele), right(scrollbox), msg);
   let elementFromPoint = x => arrowScrollbox._elementFromPoint(x);
   let nextLeftElement = () => elementFromPoint(left(scrollbox) - 1);
   let nextRightElement = () => elementFromPoint(right(scrollbox) + 1);
-  let firstScrollable = () => gBrowser.tabs[gBrowser._numPinnedTabs];
+  let firstScrollable = () => tabs[gBrowser._numPinnedTabs];
   let waitForNextFrame = async function() {
     await window.promiseDocumentFlushed(() => {});
     await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
   };
 
   arrowScrollbox.smoothScroll = false;
   registerCleanupFunction(() => {
     arrowScrollbox.smoothScroll = originalSmoothScroll;
   });
 
-  while (gBrowser.tabs.length < tabCountForOverflow) {
+  while (tabs.length < tabCountForOverflow) {
     BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
   }
 
-  gBrowser.pinTab(gBrowser.tabs[0]);
+  gBrowser.pinTab(tabs[0]);
 
   await BrowserTestUtils.waitForCondition(() => {
     return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
   });
 
   ok(!scrollbox.hasAttribute("notoverflowing"),
      "Tab strip should be overflowing");
 
@@ -56,17 +57,17 @@ add_task(async function() {
   ok(left(scrollbox) <= left(firstScrollable()), "Selecting the first tab scrolls it into view " +
      "(" + left(scrollbox) + " <= " + left(firstScrollable()) + ")");
 
   element = nextRightElement();
   EventUtils.synthesizeMouseAtCenter(downButton, {});
   await waitForNextFrame();
   isRight(element, "Scrolled one tab to the right with a single click");
 
-  gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+  gBrowser.selectedTab = tabs[tabs.length - 1];
   await waitForNextFrame();
   ok(right(gBrowser.selectedTab) <= right(scrollbox), "Selecting the last tab scrolls it into view " +
      "(" + right(gBrowser.selectedTab) + " <= " + right(scrollbox) + ")");
 
   element = nextLeftElement();
   EventUtils.synthesizeMouseAtCenter(upButton, {});
   await waitForNextFrame();
   isLeft(element, "Scrolled one tab to the left with a single click");
@@ -82,12 +83,12 @@ add_task(async function() {
   isLeft(element, "Scrolled one page of tabs with a double click");
 
   EventUtils.synthesizeMouseAtCenter(upButton, {clickCount: 3});
   await waitForNextFrame();
   var firstScrollableLeft = left(firstScrollable());
   ok(left(scrollbox) <= firstScrollableLeft, "Scrolled to the start with a triple click " +
      "(" + left(scrollbox) + " <= " + firstScrollableLeft + ")");
 
-  while (gBrowser.tabs.length > 1) {
+  while (tabs.length > 1) {
     BrowserTestUtils.removeTab(gBrowser.tabs[0]);
   }
 });
--- a/browser/base/content/test/tabs/browser_tabReorder_overflow.js
+++ b/browser/base/content/test/tabs/browser_tabReorder_overflow.js
@@ -4,41 +4,40 @@
 "use strict";
 
 requestLongerTimeout(2);
 
 add_task(async function() {
   let initialTabsLength = gBrowser.tabs.length;
 
   let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
+  let tabs = gBrowser.tabs;
   let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth);
 
   let width = ele => ele.getBoundingClientRect().width;
 
   let tabCountForOverflow = Math.ceil(width(arrowScrollbox) / tabMinWidth);
 
   let newTab1 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:robots", {skipAnimation: true});
   let newTab2 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:about", {skipAnimation: true});
   let newTab3 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:config", {skipAnimation: true});
 
-  while (gBrowser.tabs.length < tabCountForOverflow) {
+  while (tabs.length < tabCountForOverflow) {
     BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true });
   }
 
   registerCleanupFunction(function() {
-    while (gBrowser.tabs.length > initialTabsLength) {
-      gBrowser.removeTab(gBrowser.tabContainer.getItemAtIndex(initialTabsLength));
+    while (tabs.length > initialTabsLength) {
+      gBrowser.removeTab(gBrowser.tabs[initialTabsLength]);
     }
   });
 
-  let tabs = gBrowser.tabs;
-  is(tabs.length, tabCountForOverflow, "new tabs are opened");
-  is(tabs[initialTabsLength], newTab1, "newTab1 position is correct");
-  is(tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
-  is(tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
+  is(gBrowser.tabs.length, tabCountForOverflow, "new tabs are opened");
+  is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
+  is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
+  is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
 
   await dragAndDrop(newTab1, newTab2, false);
-  tabs = gBrowser.tabs;
-  is(tabs.length, tabCountForOverflow, "tabs are still there");
-  is(tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
-  is(tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
-  is(tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
+  is(gBrowser.tabs.length, tabCountForOverflow, "tabs are still there");
+  is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
+  is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
+  is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
 });
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -87,17 +87,17 @@ browser.jar:
         content/browser/sanitize.xul                  (content/sanitize.xul)
         content/browser/sanitizeDialog.js             (content/sanitizeDialog.js)
         content/browser/sanitizeDialog.css            (content/sanitizeDialog.css)
         content/browser/contentSearchUI.js            (content/contentSearchUI.js)
         content/browser/contentSearchUI.css           (content/contentSearchUI.css)
         content/browser/tabbrowser.css                (content/tabbrowser.css)
         content/browser/tabbrowser.js                 (content/tabbrowser.js)
         content/browser/tabbrowser-tab.js             (content/tabbrowser-tab.js)
-        content/browser/tabbrowser-tabs.js            (content/tabbrowser-tabs.js)
+        content/browser/tabbrowser.xml                (content/tabbrowser.xml)
 *       content/browser/urlbarBindings.xml            (content/urlbarBindings.xml)
         content/browser/utilityOverlay.js             (content/utilityOverlay.js)
         content/browser/webext-panels.js              (content/webext-panels.js)
 *       content/browser/webext-panels.xul             (content/webext-panels.xul)
         content/browser/nsContextMenu.js              (content/nsContextMenu.js)
         content/browser/contentTheme.js               (content/contentTheme.js)
 #ifdef XP_MACOSX
 # XXX: We should exclude this one as well (bug 71895)
--- a/browser/components/contextualidentity/test/browser/browser_newtabButton.js
+++ b/browser/components/contextualidentity/test/browser/browser_newtabButton.js
@@ -1,30 +1,25 @@
 "use strict";
 
 // Testing that when the user opens the add tab menu and clicks menu items
 // the correct context id is opened
 
-function findPopup(browser = gBrowser) {
-  return browser.tabContainer.querySelector(".new-tab-popup");
-}
-
 add_task(async function test_menu_with_timeout() {
   await SpecialPowers.pushPrefEnv({"set": [
       ["privacy.userContext.enabled", true],
       ["privacy.userContext.longPressBehavior", 2],
   ]});
 
-  let newTabButton = gBrowser.tabContainer.newTabButton;
+  let newTab = document.getElementById("tabbrowser-tabs");
+  let newTabButton = document.getAnonymousElementByAttribute(newTab, "anonid", "tabs-newtab-button");
   ok(newTabButton, "New tab button exists");
   ok(!newTabButton.hidden, "New tab button is visible");
-
-  await BrowserTestUtils.waitForCondition(() => !!findPopup(), "Wait for popup to exist");
-
-  let popup = findPopup();
+  await BrowserTestUtils.waitForCondition(() => !!document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"), "Wait for popup to exist");
+  let popup = document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup");
 
   for (let i = 1; i <= 4; i++) {
     let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
     EventUtils.synthesizeMouseAtCenter(newTabButton, {type: "mousedown"});
 
     await popupShownPromise;
     let contextIdItem = popup.querySelector(`menuitem[data-usercontextid="${i}"]`);
 
@@ -41,22 +36,22 @@ add_task(async function test_menu_with_t
 });
 
 add_task(async function test_menu_without_timeout() {
   await SpecialPowers.pushPrefEnv({"set": [
       ["privacy.userContext.enabled", true],
       ["privacy.userContext.longPressBehavior", 1],
   ]});
 
-  let newTabButton = gBrowser.tabContainer.newTabButton;
+  let newTab = document.getElementById("tabbrowser-tabs");
+  let newTabButton = document.getAnonymousElementByAttribute(newTab, "anonid", "tabs-newtab-button");
   ok(newTabButton, "New tab button exists");
   ok(!newTabButton.hidden, "New tab button is visible");
-
-  await BrowserTestUtils.waitForCondition(() => !!findPopup(), "Wait for popup to exist");
-  let popup = findPopup();
+  await BrowserTestUtils.waitForCondition(() => !!document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"), "Wait for popup to exist");
+  let popup = document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup");
 
   let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
   let popupHiddenPromise = BrowserTestUtils.waitForEvent(popup, "popuphidden");
   EventUtils.synthesizeMouseAtCenter(newTabButton, {type: "mousedown"});
   await popupShownPromise;
   let contextIdItems = popup.querySelectorAll("menuitem");
   // 4 + default + manage containers
   is(contextIdItems.length, 6, "Has 6 menu items");
@@ -88,29 +83,29 @@ add_task(async function test_menu_withou
 });
 
 add_task(async function test_no_menu() {
   await SpecialPowers.pushPrefEnv({"set": [
       ["privacy.userContext.enabled", true],
       ["privacy.userContext.longPressBehavior", 0],
   ]});
 
-  let newTabButton = gBrowser.tabContainer.newTabButton;
+  let newTab = document.getElementById("tabbrowser-tabs");
+  let newTabButton = document.getAnonymousElementByAttribute(newTab, "anonid", "tabs-newtab-button");
   ok(newTabButton, "New tab button exists");
   ok(!newTabButton.hidden, "New tab button is visible");
-  let popup = findPopup();
+  let popup = document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup");
   ok(!popup, "new tab should not have a popup");
 });
 
 add_task(async function test_private_mode() {
   let privateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true});
   let privateDocument = privateWindow.document;
   let {tabContainer} = privateWindow.gBrowser;
-  let newTab = tabContainer.newTabButton;
+  let newTab = privateDocument.getAnonymousElementByAttribute(tabContainer, "anonid", "tabs-newtab-button");
   let newTab2 = privateDocument.getElementById("new-tab-button");
   // Check to ensure we are talking about the right button
   ok(!!newTab.clientWidth, "new tab button should not be hidden");
   ok(!newTab2.clientWidth, "overflow new tab button should be hidden");
-  let popup = findPopup(privateWindow.gBrowser);
+  let popup = privateDocument.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup");
   ok(!popup, "new tab should not have a popup");
   await BrowserTestUtils.closeWindow(privateWindow);
 });
-
--- a/browser/components/contextualidentity/test/browser/browser_windowName.js
+++ b/browser/components/contextualidentity/test/browser/browser_windowName.js
@@ -47,18 +47,18 @@ add_task(async function test() {
 
   is(browser1.contentTitle, "?old", "Tab1 title must be 'old'");
   is(browser1.contentPrincipal.userContextId, 1, "Tab1 UCI must be 1");
 
   is(browser2.contentTitle, "?old", "Tab2 title must be 'old'");
   is(browser2.contentPrincipal.userContextId, 2, "Tab2 UCI must be 2");
 
   let found = false;
-  for (let i = 0; i < gBrowser.tabs.length; ++i) {
-    let tab = gBrowser.tabs[i];
+  for (let i = 0; i < gBrowser.tabContainer.children.length; ++i) {
+    let tab = gBrowser.tabContainer.children[i];
     let browser = gBrowser.getBrowserForTab(tab);
     if (browser.contentTitle == "?new") {
       is(browser.contentPrincipal.userContextId, 1, "Tab3 UCI must be 1");
       isnot(browser, browser1, "Tab3 is not browser 1");
       isnot(browser, browser2, "Tab3 is not browser 2");
       gBrowser.removeTab(tab);
       found = true;
       break;
--- a/browser/components/customizableui/test/browser_newtab_button_customizemode.js
+++ b/browser/components/customizableui/test/browser_newtab_button_customizemode.js
@@ -4,17 +4,17 @@
 "use strict";
 
 /**
  * Tests in this file check that user customizations to the tabstrip show
  * the correct type of new tab button while the tabstrip isn't overflowing.
  */
 
 const kGlobalNewTabButton = document.getElementById("new-tab-button");
-const kInnerNewTabButton = gBrowser.tabContainer.newTabButton;
+const kInnerNewTabButton = document.getAnonymousElementByAttribute(gBrowser.tabContainer, "anonid", "tabs-newtab-button");
 
 function assertNewTabButton(which) {
   if (which == "global") {
     isnot(kGlobalNewTabButton.getBoundingClientRect().width, 0,
       "main new tab button should be visible");
     is(kInnerNewTabButton.getBoundingClientRect().width, 0,
       "inner new tab button should be hidden");
   } else if (which == "inner") {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
@@ -1,93 +1,75 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(async function() {
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+  let window1 = await BrowserTestUtils.openNewBrowserWindow();
+  await BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.com/");
+  let window2 = await BrowserTestUtils.openNewBrowserWindow({private: true});
+  await BrowserTestUtils.openNewForegroundTab(window2.gBrowser, "http://example.com/");
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
     incognitoOverride: "spanning",
     async background() {
-      const URL = "http://example.com/";
-      let mainWindow = await browser.windows.getCurrent();
-      let newWindow = await browser.windows.create({
-        url: [URL, URL],
-      });
-      let privateWindow = await browser.windows.create({
-        incognito: true,
-        url: [URL, URL],
-      });
+      let tabs = await browser.tabs.query({url: "<all_urls>"});
+      let destination = tabs[0];
+      let source = tabs[1]; // skip over about:blank in window1
+      let privateTab = tabs[2];
+      browser.test.assertTrue(privateTab.incognito, "have a private tab.");
 
       browser.tabs.onUpdated.addListener(() => {
         // Bug 1398272: Adding onUpdated listener broke tab IDs across windows.
       });
 
-      let tab = newWindow.tabs[0].id;
-      let privateTab = privateWindow.tabs[0].id;
-
       // Assuming that this windowId does not exist.
       await browser.test.assertRejects(
-        browser.tabs.move(tab, {windowId: 123144576, index: 0}),
+        browser.tabs.move(source.id, {windowId: 123144576, index: 0}),
         /Invalid window/,
         "Should receive invalid window error");
 
       // Test that a tab cannot be moved to a private window.
-      let moved = await browser.tabs.move(tab, {windowId: privateWindow.id, index: 0});
+      let moved = await browser.tabs.move(source.id, {windowId: privateTab.windowId, index: 0});
       browser.test.assertEq(moved.length, 0, "tab was not moved to private window");
       // Test that a private tab cannot be moved to a non-private window.
-      moved = await browser.tabs.move(privateTab,
-                                      {windowId: newWindow.id, index: 0});
+      moved = await browser.tabs.move(privateTab.id, {windowId: source.windowId, index: 0});
       browser.test.assertEq(moved.length, 0, "tab was not moved from private window");
 
       // Verify tabs did not move between windows via another query.
-      let windows = await browser.windows.getAll({populate: true});
-      let newWin2 = windows.find(w => w.id === newWindow.id);
-      browser.test.assertTrue(newWin2, "Found window");
-      browser.test.assertEq(newWin2.tabs.length, 2, "Window still has two tabs");
-      for (let origTab of newWindow.tabs) {
-        browser.test.assertTrue(newWin2.tabs.find(t => t.id === origTab.id),
-                                `Window still has tab ${origTab.id}`);
+      let tabs2 = await browser.tabs.query({url: "<all_urls>"});
+      for (let i = 0; i < 3; i++) {
+        browser.test.assertEq(tabs2[i].windowId, tabs[i].windowId, "tab was not moved to another window");
+        browser.test.assertEq(tabs2[i].incognito, tabs[i].incognito, "tab privateness matches.");
       }
 
-      let privateWin2 = windows.find(w => w.id === privateWindow.id);
-      browser.test.assertTrue(privateWin2 !== null, "Found private window");
-      browser.test.assertEq(privateWin2.incognito, true,
-                            "Private window is still private");
-      browser.test.assertEq(privateWin2.tabs.length, 2,
-                            "Private window still has two tabs");
-      for (let origTab of privateWindow.tabs) {
-        browser.test.assertTrue(privateWin2.tabs.find(t => t.id === origTab.id),
-                                `Private window still has tab ${origTab.id}`);
-      }
+      browser.tabs.move(source.id, {windowId: destination.windowId, index: 0});
 
-      // Move a tab from one non-private window to another
-      await browser.tabs.move(tab, {windowId: mainWindow.id, index: 0});
-
-      mainWindow = await browser.windows.get(mainWindow.id, {populate: true});
-      browser.test.assertTrue(mainWindow.tabs.find(t => t.id === tab),
-                              "Moved tab is in main window");
-
-      newWindow = await browser.windows.get(newWindow.id, {populate: true});
-      browser.test.assertEq(newWindow.tabs.length, 1, "New window has 1 tab left");
-      browser.test.assertTrue(newWindow.tabs[0].id != tab, "Moved tab is no longer in original window");
-
-      await browser.windows.remove(newWindow.id);
-      await browser.windows.remove(privateWindow.id);
-      await browser.tabs.remove(tab);
+      tabs = await browser.tabs.query({url: "<all_urls>"});
+      browser.test.assertEq(tabs[0].url, "http://example.com/");
+      browser.test.assertEq(tabs[0].windowId, destination.windowId);
+      browser.test.assertEq(tabs[0].id, source.id);
 
       browser.test.notifyPass("tabs.move.window");
     },
   });
 
   await extension.startup();
   await extension.awaitFinish("tabs.move.window");
   await extension.unload();
+
+  for (let tab of window.gBrowser.tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+  await BrowserTestUtils.closeWindow(window1);
+  await BrowserTestUtils.closeWindow(window2);
 });
 
 add_task(async function test_currentWindowAfterTabMoved() {
   const files = {
     "current.html": "<meta charset=utf-8><script src=current.js></script>",
     "current.js": function() {
       browser.test.onMessage.addListener(msg => {
         if (msg === "current") {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
@@ -1,48 +1,43 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(async function() {
+  let window1 = await BrowserTestUtils.openNewBrowserWindow();
+  await BrowserTestUtils.openNewForegroundTab(window.gBrowser, "http://example.net/");
+  await BrowserTestUtils.openNewForegroundTab(window.gBrowser, "http://example.com/");
+  await BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.net/");
+  await BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.com/");
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
 
-    async background() {
-      const URL = "http://example.com/";
-      let mainWin = await browser.windows.getCurrent();
-      let tab1 = await browser.tabs.create({url: URL});
-      let tab2 = await browser.tabs.create({url: URL});
-
-      let newWin = await browser.windows.create({url: [URL, URL]});
-      browser.test.assertEq(newWin.tabs.length, 2, "New window has 2 tabs");
-      let [tab3, tab4] = newWin.tabs;
-
-      // move tabs in both windows to index 0 in a single call
-      await browser.tabs.move([tab2.id, tab4.id], {index: 0});
-
-      tab1 = await browser.tabs.get(tab1.id);
-      browser.test.assertEq(tab1.windowId, mainWin.id, "tab 1 is still in main window");
-
-      tab2 = await browser.tabs.get(tab2.id);
-      browser.test.assertEq(tab2.windowId, mainWin.id, "tab 2 is still in main window");
-      browser.test.assertEq(tab2.index, 0, "tab 2 moved to index 0");
-
-      tab3 = await browser.tabs.get(tab3.id);
-      browser.test.assertEq(tab3.windowId, newWin.id, "tab 3 is still in new window");
-
-      tab4 = await browser.tabs.get(tab4.id);
-      browser.test.assertEq(tab4.windowId, newWin.id, "tab 4 is still in new window");
-      browser.test.assertEq(tab4.index, 0, "tab 4 moved to index 0");
-
-      await browser.tabs.remove([tab1.id, tab2.id]);
-      await browser.windows.remove(newWin.id);
-
-      browser.test.notifyPass("tabs.move.multiple");
+    background: function() {
+      browser.tabs.query(
+        {url: "<all_urls>"},
+        tabs => {
+          let move1 = tabs[1];
+          let move3 = tabs[3];
+          browser.tabs.move([move1.id, move3.id], {index: 0});
+          browser.tabs.query(
+            {url: "<all_urls>"},
+            tabs => {
+              browser.test.assertEq(tabs[0].url, move1.url);
+              browser.test.assertEq(tabs[2].url, move3.url);
+              browser.test.notifyPass("tabs.move.multiple");
+            });
+        });
     },
   });
 
   await extension.startup();
   await extension.awaitFinish("tabs.move.multiple");
   await extension.unload();
+
+  for (let tab of window.gBrowser.tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+  await BrowserTestUtils.closeWindow(window1);
 });
--- a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
@@ -1,37 +1,42 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(async function() {
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+  let window1 = await BrowserTestUtils.openNewBrowserWindow();
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(window1.gBrowser, "http://example.com/");
+  window1.gBrowser.pinTab(tab1);
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["tabs"],
     },
 
-    async background() {
-      const URL = "http://example.com/";
-
-      let mainWin = await browser.windows.getCurrent();
-      let tab = await browser.tabs.create({url: URL});
-
-      let newWin = await browser.windows.create({url: URL});
-      let tab2 = newWin.tabs[0];
-      await browser.tabs.update(tab2.id, {pinned: true});
+    background: function() {
+      browser.tabs.query(
+        {url: "<all_urls>"},
+        tabs => {
+          let destination = tabs[0];
+          let source = tabs[1]; // remember, pinning moves it to the left.
+          browser.tabs.move(source.id, {windowId: destination.windowId, index: 0});
 
-      // Try to move a tab before the pinned tab.  The move should be ignored.
-      let moved = await browser.tabs.move(tab.id, {windowId: newWin.id, index: 0});
-      browser.test.assertEq(moved.length, 0, "move() returned no moved tab");
-
-      tab = await browser.tabs.get(tab.id);
-      browser.test.assertEq(tab.windowId, mainWin.id, "Tab stayed in its original window");
-
-      await browser.tabs.remove(tab.id);
-      await browser.windows.remove(newWin.id);
-      browser.test.notifyPass("tabs.move.pin");
+          browser.tabs.query(
+            {url: "<all_urls>"},
+            tabs => {
+              browser.test.assertEq(true, tabs[0].pinned);
+              browser.test.notifyPass("tabs.move.pin");
+            });
+        });
     },
   });
 
   await extension.startup();
   await extension.awaitFinish("tabs.move.pin");
   await extension.unload();
+
+  for (let tab of window.gBrowser.tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+  await BrowserTestUtils.closeWindow(window1);
 });
--- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -85,17 +85,17 @@ add_task(async function test_sidebarpane
 
     await testPlacesPanel(test, () => {
       changeSidebarDirection("rtl");
       info("Running " + test.desc + " in RTL mode");
     });
 
     // Remove tabs created by sub-tests.
     while (gBrowser.tabs.length > 1) {
-      gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+      gBrowser.removeTab(gBrowser.tabContainer.lastElementChild);
     }
   }
 });
 
 async function testPlacesPanel(testInfo, preFunc) {
   await testInfo.init();
 
   let promise = new Promise(resolve => {
--- a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
+++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js
@@ -145,17 +145,17 @@ function loadTab(tab, url) {
 function ensure_opentabs_match_db() {
   let tabs = {};
 
   for (let browserWin of Services.wm.getEnumerator("navigator:browser")) {
     // skip closed-but-not-destroyed windows
     if (browserWin.closed)
       continue;
 
-    for (let i = 0; i < browserWin.gBrowser.tabs.length; i++) {
+    for (let i = 0; i < browserWin.gBrowser.tabContainer.childElementCount; i++) {
       let browser = browserWin.gBrowser.getBrowserAtIndex(i);
       let url = browser.currentURI.spec;
       if (browserWin.isBlankPageURL(url))
         continue;
       if (!(url in tabs))
         tabs[url] = 1;
       else
         tabs[url]++;
--- a/browser/modules/test/browser/browser_taskbar_preview.js
+++ b/browser/modules/test/browser/browser_taskbar_preview.js
@@ -53,21 +53,21 @@ function test() {
   ok(gBrowser.tabs.length == 3, "Successfully closed a tab");
 
   // Select #1
   ok(getPreviewForTab(gBrowser.tabs[0]).controller.onActivate(), "Activation was accepted");
   ok(gBrowser.tabs[0].selected, "Correct tab was selected");
   checkSelectedTab();
 
   // Remove #3 (non active)
-  gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+  gBrowser.removeTab(gBrowser.tabContainer.lastElementChild);
   checkPreviews(2, "Expected number of previews after closing unselected via browser");
 
   // Remove #1 (active)
-  gBrowser.removeTab(gBrowser.tabs[0]);
+  gBrowser.removeTab(gBrowser.tabContainer.firstElementChild);
   checkPreviews(1, "Expected number of previews after closing selected tab via browser");
 
   // Add a new tab
   BrowserTestUtils.addTab(gBrowser);
   checkPreviews(2);
   // Check default selection
   checkSelectedTab();
 
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -44,17 +44,17 @@
     /* Value for --in-content-page-background for dark mode in in-content/common.inc.css */
     background-color: #2A2A2E;
   }
 }
 }
 
 #tabbrowser-tabs,
 #tabbrowser-tabs > .tabbrowser-arrowscrollbox,
-#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] {
+#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
   min-height: var(--tab-min-height);
 }
 
 .tab-stack {
   min-height: inherit;
 }
 
 @supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") {
@@ -532,18 +532,18 @@
 }
 
 /*
  * LightweightThemeConsumer will set the current lightweight theme's header
  * image to the lwt-header-image variable, used in each of the following rulesets.
  */
 
 /* Lightweight theme on tabs */
-#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-background[multiselected=true]:-moz-lwtheme,
-#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-background[selected=true]:-moz-lwtheme {
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab > .tab-stack > .tab-background[multiselected=true]:-moz-lwtheme,
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab > .tab-stack > .tab-background[selected=true]:-moz-lwtheme {
   background-attachment: scroll, scroll, fixed;
   background-color: transparent;
   background-image: linear-gradient(var(--lwt-selected-tab-background-color, transparent), var(--lwt-selected-tab-background-color, transparent)),
                     linear-gradient(var(--toolbar-bgcolor), var(--toolbar-bgcolor)),
                     var(--lwt-header-image, none);
   background-position: 0 0, 0 0, right top;
   background-repeat: repeat-x, repeat-x, no-repeat;
   background-size: auto 100%, auto 100%, auto auto;
@@ -577,29 +577,29 @@
 
 .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-line[multiselected]:not([selected=true]) {
   opacity: 0.5;
 }
 
 /* Pinned tabs */
 
 /* Pinned tab separators need position: absolute when positioned (during overflow). */
-#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned]::after {
+#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned]::after {
   position: absolute;
   top: 0;
   bottom: 0;
   right: 0;
 }
 
-#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned]:-moz-locale-dir(rtl)::after {
+#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned]:-moz-locale-dir(rtl)::after {
   right: unset;
   left: 0;
 }
 
-#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] > .tab-stack {
+#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] > .tab-stack {
   border-inline-end: 1px solid transparent;
 }
 
 .tabbrowser-tab:-moz-any([image], [pinned]) > .tab-stack > .tab-content[attention]:not([selected="true"]),
 .tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected="true"]) {
   background-image: url(chrome://browser/skin/tabbrowser/indicator-tab-attention.svg);
   background-position: center bottom calc(-4px + var(--tabs-navbar-shadow-size));
   background-repeat: no-repeat;
@@ -659,36 +659,36 @@
 
 %ifdef MENUBAR_CAN_AUTOHIDE
 :root[tabsintitlebar]:not([extradragspace]) #toolbar-menubar[autohide=true] + #TabsToolbar .tabbrowser-tab::after,
 %else
 :root[tabsintitlebar]:not([extradragspace]) .tabbrowser-tab::after,
 %endif
 /* Show full height tab separators on hover and multiselection. */
 .tabbrowser-tab:hover::after,
-#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab[beforehovered]::after,
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforehovered]::after,
 .tabbrowser-tab[multiselected]::after,
-#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab[before-multiselected]::after {
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[before-multiselected]::after {
   margin-top: var(--tabs-top-border-width);
   margin-bottom: 0;
 }
 
 /* Show full height tab separators on selected tabs. */
-#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab[beforeselected-visible]::after,
-#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[visuallyselected]::before,
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforeselected-visible]::after,
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[visuallyselected]::before,
 .tabbrowser-tab[visuallyselected]::after {
   border-color: var(--tabs-border-color);
   margin-top: 0;
   margin-bottom: var(--tabs-navbar-shadow-size);
   opacity: 1;
 }
 
 .tabbrowser-tab::after,
 /* Also show separators beside the selected tab when dragging it. */
-#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[visuallyselected]::before {
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[visuallyselected]::before {
   content: "";
   display: -moz-box;
 }
 
 /* Tab bar scroll arrows */
 
 .tabbrowser-arrowscrollbox > .scrollbutton-up,
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -181,17 +181,17 @@ PushRecord.prototype = {
   },
 
   isTabOpen() {
     for (let window of Services.wm.getEnumerator("navigator:browser")) {
       if (window.closed || PrivateBrowsingUtils.isWindowPrivate(window)) {
         continue;
       }
       // `gBrowser` on Desktop; `BrowserApp` on Fennec.
-      let tabs = window.gBrowser ? window.gBrowser.tabs :
+      let tabs = window.gBrowser ? window.gBrowser.tabContainer.children :
                  window.BrowserApp.tabs;
       for (let tab of tabs) {
         // `linkedBrowser` on Desktop; `browser` on Fennec.
         let tabURI = (tab.linkedBrowser || tab.browser).currentURI;
         if (tabURI.prePath == this.uri.prePath) {
           return true;
         }
       }
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -559,17 +559,17 @@ Tester.prototype = {
     // eslint-disable-next-line no-nested-ternary
     let baseMsg = timedOut ? "Found a {elt} after previous test timed out"
                            : this.currentTest ? "Found an unexpected {elt} at the end of test run"
                                               : "Found an unexpected {elt}";
 
     // Remove stale tabs
     if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) {
       while (gBrowser.tabs.length > 1) {
-        let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+        let lastTab = gBrowser.tabContainer.lastElementChild;
         if (!lastTab.closing) {
           // Report the stale tab as an error only when they're not closing.
           // Tests can finish without waiting for the closing tabs.
           this.currentTest.addResult(new testResult({
             name: baseMsg.replace("{elt}", "tab") + ": " +
               lastTab.linkedBrowser.currentURI.spec,
             allowFailure: this.currentTest.allowFailure,
           }));
--- a/toolkit/components/contextualidentity/ContextualIdentityService.jsm
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
@@ -482,18 +482,18 @@ function _ContextualIdentityService(path
 
   _forEachContainerTab(callback, userContextId = 0) {
     for (let win of Services.wm.getEnumerator("navigator:browser")) {
       if (win.closed || !win.gBrowser) {
         continue;
       }
 
       let tabbrowser = win.gBrowser;
-      for (let i = tabbrowser.tabs.length - 1; i >= 0; --i) {
-        let tab = tabbrowser.tabs[i];
+      for (let i = tabbrowser.tabContainer.children.length - 1; i >= 0; --i) {
+        let tab = tabbrowser.tabContainer.children[i];
         if (tab.hasAttribute("usercontextid") &&
                   (!userContextId ||
                    parseInt(tab.getAttribute("usercontextid"), 10) == userContextId)) {
           callback(tab, tabbrowser);
         }
       }
     }
   },
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -66,16 +66,17 @@ toolkit.jar:
    content/global/bindings/datekeeper.js       (widgets/datekeeper.js)
    content/global/bindings/datepicker.js       (widgets/datepicker.js)
    content/global/bindings/datetimebox.css     (widgets/datetimebox.css)
    content/global/bindings/general.xml         (widgets/general.xml)
    content/global/bindings/popup.xml           (widgets/popup.xml)
    content/global/bindings/richlistbox.xml     (widgets/richlistbox.xml)
    content/global/bindings/scrollbox.xml       (widgets/scrollbox.xml)
    content/global/bindings/spinner.js          (widgets/spinner.js)
+   content/global/bindings/tabbox.xml          (widgets/tabbox.xml)
 *  content/global/bindings/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
    content/global/bindings/timepicker.js       (widgets/timepicker.js)
    content/global/elements/autocomplete-popup.js              (widgets/autocomplete-popup.js)
    content/global/elements/autocomplete-richlistitem.js       (widgets/autocomplete-richlistitem.js)
    content/global/elements/browser-custom-element.js          (widgets/browser-custom-element.js)
    content/global/elements/button.js           (widgets/button.js)
    content/global/elements/checkbox.js         (widgets/checkbox.js)
--- a/toolkit/content/tests/chrome/test_tabbox.xul
+++ b/toolkit/content/tests/chrome/test_tabbox.xul
@@ -77,44 +77,44 @@
 SimpleTest.waitForExplicitFinish();
 
 function test_tabbox()
 {
   var tabbox = document.getElementById("tabbox");
   var tabs = document.getElementById("tabs");
   var tabpanels = document.getElementById("tabpanels");
 
-  test_tabbox_State(tabbox, "tabbox initial", 0, tabs.allTabs[0], tabpanels.firstChild);
+  test_tabbox_State(tabbox, "tabbox initial", 0, tabs.firstChild, tabpanels.firstChild);
 
   // check the selectedIndex property
   tabbox.selectedIndex = 1;
-  test_tabbox_State(tabbox, "tabbox selectedIndex 1", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild);
+  test_tabbox_State(tabbox, "tabbox selectedIndex 1", 1, tabs.lastChild, tabpanels.lastChild);
 
   tabbox.selectedIndex = 2;
-  test_tabbox_State(tabbox, "tabbox selectedIndex 2", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild);
+  test_tabbox_State(tabbox, "tabbox selectedIndex 2", 1, tabs.lastChild, tabpanels.lastChild);
 
   // tabbox must have a selection, so setting to -1 should do nothing
   tabbox.selectedIndex = -1;
-  test_tabbox_State(tabbox, "tabbox selectedIndex -1", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild);
+  test_tabbox_State(tabbox, "tabbox selectedIndex -1", 1, tabs.lastChild, tabpanels.lastChild);
 
   // check the selectedTab property
-  tabbox.selectedTab = tabs.allTabs[0];
-  test_tabbox_State(tabbox, "tabbox selected", 0, tabs.allTabs[0], tabpanels.firstChild);
+  tabbox.selectedTab = tabs.firstChild;
+  test_tabbox_State(tabbox, "tabbox selected", 0, tabs.firstChild, tabpanels.firstChild);
 
   // setting selectedTab to null should not do anything
   tabbox.selectedTab = null;
-  test_tabbox_State(tabbox, "tabbox selectedTab null", 0, tabs.allTabs[0], tabpanels.firstChild);
+  test_tabbox_State(tabbox, "tabbox selectedTab null", 0, tabs.firstChild, tabpanels.firstChild);
 
   // check the selectedPanel property
   tabbox.selectedPanel = tabpanels.lastChild;
-  test_tabbox_State(tabbox, "tabbox selectedPanel", 0, tabs.allTabs[0], tabpanels.lastChild);
+  test_tabbox_State(tabbox, "tabbox selectedPanel", 0, tabs.firstChild, tabpanels.lastChild);
 
   // setting selectedPanel to null should not do anything
   tabbox.selectedPanel = null;
-  test_tabbox_State(tabbox, "tabbox selectedPanel null", 0, tabs.allTabs[0], tabpanels.lastChild);
+  test_tabbox_State(tabbox, "tabbox selectedPanel null", 0, tabs.firstChild, tabpanels.lastChild);
 
   tabbox.selectedIndex = 0;
   test_tabpanels(tabpanels, tabbox);
 
   tabs.firstChild.remove();
   tabs.firstChild.remove();
 
   test_tabs(tabs);
--- a/toolkit/content/widgets/tabbox.js
+++ b/toolkit/content/widgets/tabbox.js
@@ -228,17 +228,17 @@ class MozTabpanels extends MozXULElement
       return null;
 
     // Return tab element having 'linkedpanel' attribute equal to the id
     // of the tab panel or the same index as the tab panel element.
     let tabpanelIdx = Array.prototype.indexOf.call(this.children, aTabPanelElm);
     if (tabpanelIdx == -1)
       return null;
 
-    let tabElms = tabsElm.allTabs;
+    let tabElms = tabsElm.children;
     let tabElmFromIndex = tabElms[tabpanelIdx];
 
     let tabpanelId = aTabPanelElm.id;
     if (tabpanelId) {
       for (let idx = 0; idx < tabElms.length; idx++) {
         let tabElm = tabElms[idx];
         if (tabElm.linkedPanel == tabpanelId)
           return tabElm;
@@ -319,17 +319,17 @@ MozElements.MozTab = class MozTab extend
 
     let stopwatchid = this.parentNode.getAttribute("stopwatchid");
     if (stopwatchid) {
       TelemetryStopwatch.start(stopwatchid);
     }
 
     // Call this before setting the 'ignorefocus' attribute because this
     // will pass on focus if the formerly selected tab was focused as well.
-    this.closest("tabs")._selectNewTab(this);
+    this.parentNode._selectNewTab(this);
 
     var isTabFocused = false;
     try {
       isTabFocused = (document.commandDispatcher.focusedElement == this);
     } catch (e) {}
 
     // Set '-moz-user-focus' to 'ignore' so that PostHandleEvent() can't
     // focus the tab; we only want tabs to be focusable by the mouse if
@@ -347,48 +347,48 @@ MozElements.MozTab = class MozTab extend
 
   on_keydown(event) {
     if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
       return;
     }
     switch (event.keyCode) {
       case KeyEvent.DOM_VK_LEFT: {
         let direction = window.getComputedStyle(this.parentNode).direction;
-        this.container.advanceSelectedTab(direction == "ltr" ? -1 : 1,
+        this.parentNode.advanceSelectedTab(direction == "ltr" ? -1 : 1,
                                            this.arrowKeysShouldWrap);
         event.preventDefault();
         break;
       }
 
       case KeyEvent.DOM_VK_RIGHT: {
         let direction = window.getComputedStyle(this.parentNode).direction;
-        this.container.advanceSelectedTab(direction == "ltr" ? 1 : -1,
+        this.parentNode.advanceSelectedTab(direction == "ltr" ? 1 : -1,
                                            this.arrowKeysShouldWrap);
         event.preventDefault();
         break;
       }
 
       case KeyEvent.DOM_VK_UP:
-        this.container.advanceSelectedTab(-1, this.arrowKeysShouldWrap);
+        this.parentNode.advanceSelectedTab(-1, this.arrowKeysShouldWrap);
         event.preventDefault();
         break;
 
       case KeyEvent.DOM_VK_DOWN:
-        this.container.advanceSelectedTab(1, this.arrowKeysShouldWrap);
+        this.parentNode.advanceSelectedTab(1, this.arrowKeysShouldWrap);
         event.preventDefault();
         break;
 
       case KeyEvent.DOM_VK_HOME:
-        this.container._selectNewTab(this.container.allTabs[0]);
+        this.parentNode._selectNewTab(this.parentNode.children[0]);
         event.preventDefault();
         break;
 
       case KeyEvent.DOM_VK_END: {
-        let {allTabs} = this.container;
-        this.container._selectNewTab(allTabs[allTabs.length - 1], -1);
+        let tabs = this.parentNode.children;
+        this.parentNode._selectNewTab(tabs[tabs.length - 1], -1);
         event.preventDefault();
         break;
       }
     }
   }
 
   set value(val) {
     this.setAttribute("value", val);
@@ -426,359 +426,9 @@ MozElements.MozTab = class MozTab extend
 
   get linkedPanel() {
     return this.getAttribute("linkedpanel");
   }
 };
 
 MozXULElement.implementCustomInterface(MozElements.MozTab, [Ci.nsIDOMXULSelectControlItemElement]);
 customElements.define("tab", MozElements.MozTab);
-
-class TabsBase extends MozElements.BaseControl {
-  constructor() {
-    super();
-
-    this.addEventListener("DOMMouseScroll", (event) => {
-      if (Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling")) {
-        if (event.detail > 0) {
-          this.advanceSelectedTab(1, false);
-        } else {
-          this.advanceSelectedTab(-1, false);
-        }
-        event.stopPropagation();
-      }
-    });
-  }
-
-  // to be called from derived class connectedCallback
-  baseConnect() {
-    this._tabbox = null;
-    this.ACTIVE_DESCENDANT_ID = "keyboard-focused-tab-" + Math.trunc(Math.random() * 1000000);
-
-    if (!this.hasAttribute("orient"))
-      this.setAttribute("orient", "horizontal");
-
-    if (this.tabbox && this.tabbox.hasAttribute("selectedIndex")) {
-      let selectedIndex = parseInt(this.tabbox.getAttribute("selectedIndex"));
-      this.selectedIndex = selectedIndex > 0 ? selectedIndex : 0;
-      return;
-    }
-
-    let children = this.allTabs;
-    let length = children.length;
-    for (var i = 0; i < length; i++) {
-      if (children[i].getAttribute("selected") == "true") {
-        this.selectedIndex = i;
-        return;
-      }
-    }
-
-    var value = this.value;
-    if (value)
-      this.value = value;
-    else
-      this.selectedIndex = 0;
-  }
-
-  /**
-   * nsIDOMXULSelectControlElement
-   */
-  get itemCount() {
-    return this.allTabs.length;
-  }
-
-  set value(val) {
-    this.setAttribute("value", val);
-    var children = this.allTabs;
-    for (var c = children.length - 1; c >= 0; c--) {
-      if (children[c].value == val) {
-        this.selectedIndex = c;
-        break;
-      }
-    }
-    return val;
-  }
-
-  get value() {
-    return this.getAttribute("value");
-  }
-
-  get tabbox() {
-    if (!this._tabbox) {
-      // Memoize the result in a field rather than replacing this property,
-      // so that it can be reset along with the binding.
-      this._tabbox = this.closest("tabbox");
-    }
-
-    return this._tabbox;
-  }
-
-  set selectedIndex(val) {
-    var tab = this.getItemAtIndex(val);
-    if (tab) {
-      for (let otherTab of this.allTabs) {
-        if (otherTab != tab && otherTab.selected) {
-          otherTab._selected = false;
-        }
-      }
-      tab._selected = true;
-
-      this.setAttribute("value", tab.value);
-
-      let linkedPanel = this.getRelatedElement(tab);
-      if (linkedPanel) {
-        this.tabbox.setAttribute("selectedIndex", val);
-
-        // This will cause an onselect event to fire for the tabpanel
-        // element.
-        this.tabbox.tabpanels.selectedPanel = linkedPanel;
-      }
-    }
-    return val;
-  }
-
-  get selectedIndex() {
-    const tabs = this.allTabs;
-    for (var i = 0; i < tabs.length; i++) {
-      if (tabs[i].selected)
-        return i;
-    }
-    return -1;
-  }
-
-  set selectedItem(val) {
-    if (val && !val.selected)
-      // The selectedIndex setter ignores invalid values
-      // such as -1 if |val| isn't one of our child nodes.
-      this.selectedIndex = this.getIndexOfItem(val);
-    return val;
-  }
-
-  get selectedItem() {
-    const tabs = this.allTabs;
-    for (var i = 0; i < tabs.length; i++) {
-      if (tabs[i].selected)
-        return tabs[i];
-    }
-    return null;
-  }
-
-  get ariaFocusedIndex() {
-    const tabs = this.allTabs;
-    for (var i = 0; i < tabs.length; i++) {
-      if (tabs[i].id == this.ACTIVE_DESCENDANT_ID)
-        return i;
-    }
-    return -1;
-  }
-
-  set ariaFocusedItem(val) {
-    let setNewItem = val && this.getIndexOfItem(val) != -1;
-    let clearExistingItem = this.ariaFocusedItem && (!val || setNewItem);
-    if (clearExistingItem) {
-      let ariaFocusedItem = this.ariaFocusedItem;
-      ariaFocusedItem.classList.remove("keyboard-focused-tab");
-      ariaFocusedItem.id = "";
-      this.selectedItem.removeAttribute("aria-activedescendant");
-    }
-
-    if (setNewItem) {
-      this.ariaFocusedItem = null;
-      val.id = this.ACTIVE_DESCENDANT_ID;
-      val.classList.add("keyboard-focused-tab");
-      this.selectedItem.setAttribute("aria-activedescendant", this.ACTIVE_DESCENDANT_ID);
-    }
-
-    return val;
-  }
-
-  get ariaFocusedItem() {
-    return document.getElementById(this.ACTIVE_DESCENDANT_ID);
-  }
-
-  /**
-   * nsIDOMXULRelatedElement
-   */
-  getRelatedElement(aTabElm) {
-    if (!aTabElm)
-      return null;
-
-    let tabboxElm = this.tabbox;
-    if (!tabboxElm)
-      return null;
-
-    let tabpanelsElm = tabboxElm.tabpanels;
-    if (!tabpanelsElm)
-      return null;
-
-    // Get linked tab panel by 'linkedpanel' attribute on the given tab
-    // element.
-    let linkedPanelId = aTabElm.linkedPanel;
-    if (linkedPanelId) {
-      return this.ownerDocument.getElementById(linkedPanelId);
-    }
-
-    // otherwise linked tabpanel element has the same index as the given
-    // tab element.
-    let tabElmIdx = this.getIndexOfItem(aTabElm);
-    return tabpanelsElm.children[tabElmIdx];
-  }
-
-  getIndexOfItem(item) {
-    return Array.prototype.indexOf.call(this.allTabs, item);
-  }
-
-  getItemAtIndex(index) {
-    return this.allTabs[index] || null;
-  }
-
-  /**
-   * Find an adjacent tab.
-   *
-   * @param {Node} startTab         A <tab> element to start searching from.
-   * @param {Number} opts.direction 1 to search forward, -1 to search backward.
-   * @param {Boolean} opts.wrap     If true, wrap around if the search reaches
-   *                                the end (or beginning) of the tab strip.
-   * @param {Boolean} opts.startWithAdjacent
-   *                                If true (which is the default), start
-   *                                searching from the  next tab after (or
-   *                                before) startTab.  If false, startTab may
-   *                                be returned if it passes the filter.
-   * @param {Boolean} opts.advance  If false, start searching with startTab.  If
-   *                                true, start searching with an adjacent tab.
-   * @param {Function} opts.filter  A function to select which tabs to return.
-   *
-   * @return {Node | null}     The next <tab> element or, if none exists, null.
-   */
-  findNextTab(startTab, opts = {}) {
-    let {
-      direction = 1,
-      wrap = false,
-      startWithAdjacent = true,
-      filter = tab => true,
-    } = opts;
-
-    let tab = startTab;
-    if (!startWithAdjacent && filter(tab)) {
-      return tab;
-    }
-
-    let children = this.allTabs;
-    let i = children.indexOf(tab);
-    if (i < 0) {
-      return null;
-    }
-
-    while (true) {
-      i += direction;
-      if (wrap) {
-        if (i < 0) {
-          i = children.length - 1;
-        } else if (i >= children.length) {
-          i = 0;
-        }
-      } else if (i < 0 || i >= children.length) {
-        return null;
-      }
-
-      tab = children[i];
-      if (tab == startTab) {
-        return null;
-      }
-      if (filter(tab)) {
-        return tab;
-      }
-    }
-  }
-
-  _selectNewTab(aNewTab, aFallbackDir, aWrap) {
-    this.ariaFocusedItem = null;
-
-    aNewTab = this.findNextTab(aNewTab, {
-      direction: aFallbackDir,
-      wrap: aWrap,
-      startWithAdjacent: false,
-      filter: tab => !tab.hidden && !tab.disabled && this._canAdvanceToTab(tab),
-    });
-
-    var isTabFocused = false;
-    try {
-      isTabFocused =
-      (document.commandDispatcher.focusedElement == this.selectedItem);
-    } catch (e) {}
-    this.selectedItem = aNewTab;
-    if (isTabFocused) {
-      aNewTab.focus();
-    } else if (this.getAttribute("setfocus") != "false") {
-      let selectedPanel = this.tabbox.selectedPanel;
-      document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel);
-
-      // Make sure that the focus doesn't move outside the tabbox
-      if (this.tabbox) {
-        try {
-          let el = document.commandDispatcher.focusedElement;
-          while (el && el != this.tabbox.tabpanels) {
-            if (el == this.tabbox || el == selectedPanel)
-              return;
-            el = el.parentNode;
-          }
-          aNewTab.focus();
-        } catch (e) {}
-      }
-    }
-  }
-
-  _canAdvanceToTab(aTab) {
-    return true;
-  }
-
-  advanceSelectedTab(aDir, aWrap) {
-    let startTab = this.ariaFocusedItem || this.selectedItem;
-    let newTab = this.findNextTab(startTab, {
-      direction: aDir,
-      wrap: aWrap,
-    });
-    this._selectNewTab(newTab, aDir, aWrap);
-  }
-
-  appendItem(label, value) {
-    var tab = document.createXULElement("tab");
-    tab.setAttribute("label", label);
-    tab.setAttribute("value", value);
-    this.appendChild(tab);
-    return tab;
-  }
 }
-
-MozXULElement.implementCustomInterface(TabsBase, [Ci.nsIDOMXULSelectControlElement, Ci.nsIDOMXULRelatedElement]);
-
-MozElements.TabsBase = TabsBase;
-
-class MozTabs extends TabsBase {
-  connectedCallback() {
-    if (this.delayConnectedCallback()) {
-      return;
-    }
-
-    let start = MozXULElement.parseXULToFragment(`<spacer class="tabs-left"/>`);
-    this.insertBefore(start, this.firstChild);
-
-    let end = MozXULElement.parseXULToFragment(`<spacer class="tabs-right" flex="1"/>`);
-    this.insertBefore(end, null);
-
-    this.baseConnect();
-  }
-
-  // Accessor for tabs.  This element has spacers as the first and
-  // last elements and <tab>s are everything in between.
-  get allTabs() {
-    let children = Array.from(this.children);
-    return children.splice(1, children.length - 2);
-  }
-
-  appendChild(tab) {
-    // insert before the end spacer.
-    this.insertBefore(tab, this.lastChild);
-  }
-}
-
-customElements.define("tabs", MozTabs);
-}
new file mode 100644
--- /dev/null
+++ b/toolkit/content/widgets/tabbox.xml
@@ -0,0 +1,377 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+
+<bindings id="tabBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
+  <binding id="tabs"
+           extends="chrome://global/content/bindings/general.xml#basecontrol">
+    <content>
+      <xul:spacer class="tabs-left"/>
+      <children/>
+      <xul:spacer class="tabs-right" flex="1"/>
+    </content>
+
+    <implementation implements="nsIDOMXULSelectControlElement, nsIDOMXULRelatedElement">
+      <constructor>
+      <![CDATA[
+
+        if (!this.hasAttribute("orient"))
+          this.setAttribute("orient", "horizontal");
+
+        if (this.tabbox && this.tabbox.hasAttribute("selectedIndex")) {
+          let selectedIndex = parseInt(this.tabbox.getAttribute("selectedIndex"));
+          this.selectedIndex = selectedIndex > 0 ? selectedIndex : 0;
+          return;
+        }
+
+        var children = this.children;
+        var length = children.length;
+        for (var i = 0; i < length; i++) {
+          if (children[i].getAttribute("selected") == "true") {
+            this.selectedIndex = i;
+            return;
+          }
+        }
+
+        var value = this.value;
+        if (value)
+          this.value = value;
+        else
+          this.selectedIndex = 0;
+      ]]>
+      </constructor>
+
+      <!-- nsIDOMXULRelatedElement -->
+      <method name="getRelatedElement">
+        <parameter name="aTabElm"/>
+        <body>
+        <![CDATA[
+          if (!aTabElm)
+            return null;
+
+          let tabboxElm = this.tabbox;
+          if (!tabboxElm)
+            return null;
+
+          let tabpanelsElm = tabboxElm.tabpanels;
+          if (!tabpanelsElm)
+            return null;
+
+          // Get linked tab panel by 'linkedpanel' attribute on the given tab
+          // element.
+          let linkedPanelId = aTabElm.linkedPanel;
+          if (linkedPanelId) {
+            let ownerDoc = this.ownerDocument;
+
+            // XXX bug 565858: if XUL tab element is anonymous element then
+            // suppose linked tab panel is hosted within the same XBL binding
+            // and search it by ID attribute inside an anonymous content of
+            // the binding. This is not robust assumption since tab elements may
+            // live outside a tabbox element so that for example tab elements
+            // can be explicit content but tab panels can be anonymous.
+
+            let bindingParent = ownerDoc.getBindingParent(aTabElm);
+            if (bindingParent)
+              return ownerDoc.getAnonymousElementByAttribute(bindingParent,
+                                                             "id",
+                                                             linkedPanelId);
+
+            return ownerDoc.getElementById(linkedPanelId);
+          }
+
+          // otherwise linked tabpanel element has the same index as the given
+          // tab element.
+          let tabElmIdx = this.getIndexOfItem(aTabElm);
+          return tabpanelsElm.children[tabElmIdx];
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULSelectControlElement -->
+      <property name="itemCount" readonly="true"
+                onget="return this.children.length"/>
+
+      <property name="value" onget="return this.getAttribute('value');">
+        <setter>
+          <![CDATA[
+            this.setAttribute("value", val);
+            var children = this.children;
+            for (var c = children.length - 1; c >= 0; c--) {
+              if (children[c].value == val) {
+                this.selectedIndex = c;
+                break;
+              }
+            }
+            return val;
+          ]]>
+        </setter>
+      </property>
+
+      <field name="_tabbox">null</field>
+      <property name="tabbox" readonly="true">
+        <getter><![CDATA[
+          // Memoize the result in a field rather than replacing this property,
+          // so that it can be reset along with the binding.
+          if (this._tabbox) {
+            return this._tabbox;
+          }
+
+          let parent = this.parentNode;
+          while (parent) {
+            if (parent.localName == "tabbox") {
+              break;
+            }
+            parent = parent.parentNode;
+          }
+
+          return this._tabbox = parent;
+        ]]></getter>
+      </property>
+
+      <!-- _tabbox is deprecated, it exists only for backwards compatibility. -->
+      <field name="_tabbox" readonly="true"><![CDATA[
+        this.tabbox;
+      ]]></field>
+
+      <field name="_prefService" readonly="true"><![CDATA[
+        Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+      ]]></field>
+
+      <property name="selectedIndex">
+        <getter>
+        <![CDATA[
+          const tabs = this.children;
+          for (var i = 0; i < tabs.length; i++) {
+            if (tabs[i].selected)
+              return i;
+          }
+          return -1;
+        ]]>
+        </getter>
+
+        <setter>
+        <![CDATA[
+          var tab = this.getItemAtIndex(val);
+          if (tab) {
+            for (let otherTab of this.children) {
+              if (otherTab != tab && otherTab.selected) {
+                otherTab._selected = false;
+              }
+            }
+            tab._selected = true;
+
+            this.setAttribute("value", tab.value);
+
+            let linkedPanel = this.getRelatedElement(tab);
+            if (linkedPanel) {
+              this.tabbox.setAttribute("selectedIndex", val);
+
+              // This will cause an onselect event to fire for the tabpanel
+              // element.
+              this.tabbox.tabpanels.selectedPanel = linkedPanel;
+            }
+          }
+          return val;
+        ]]>
+        </setter>
+      </property>
+
+      <property name="selectedItem">
+        <getter>
+        <![CDATA[
+          const tabs = this.children;
+          for (var i = 0; i < tabs.length; i++) {
+            if (tabs[i].selected)
+              return tabs[i];
+          }
+          return null;
+        ]]>
+        </getter>
+
+        <setter>
+        <![CDATA[
+          if (val && !val.selected)
+            // The selectedIndex setter ignores invalid values
+            // such as -1 if |val| isn't one of our child nodes.
+            this.selectedIndex = this.getIndexOfItem(val);
+          return val;
+        ]]>
+        </setter>
+      </property>
+
+      <field name="ACTIVE_DESCENDANT_ID" readonly="true"><![CDATA[
+        "keyboard-focused-tab-" + Math.trunc(Math.random() * 1000000);
+      ]]></field>
+
+      <property name="ariaFocusedIndex" readonly="true">
+        <getter>
+        <![CDATA[
+          const tabs = this.children;
+          for (var i = 0; i < tabs.length; i++) {
+            if (tabs[i].id == this.ACTIVE_DESCENDANT_ID)
+              return i;
+          }
+          return -1;
+        ]]>
+        </getter>
+      </property>
+
+      <property name="ariaFocusedItem">
+        <getter>
+        <![CDATA[
+          return document.getElementById(this.ACTIVE_DESCENDANT_ID);
+        ]]>
+        </getter>
+
+        <setter>
+        <![CDATA[
+          let setNewItem = val && this.getIndexOfItem(val) != -1;
+          let clearExistingItem = this.ariaFocusedItem && (!val || setNewItem);
+          if (clearExistingItem) {
+            let ariaFocusedItem = this.ariaFocusedItem;
+            ariaFocusedItem.classList.remove("keyboard-focused-tab");
+            ariaFocusedItem.id = "";
+            this.selectedItem.removeAttribute("aria-activedescendant");
+          }
+
+          if (setNewItem) {
+            this.ariaFocusedItem = null;
+            val.id = this.ACTIVE_DESCENDANT_ID;
+            val.classList.add("keyboard-focused-tab");
+            this.selectedItem.setAttribute("aria-activedescendant", this.ACTIVE_DESCENDANT_ID);
+          }
+
+          return val;
+        ]]>
+        </setter>
+      </property>
+
+      <method name="getIndexOfItem">
+        <parameter name="item"/>
+        <body>
+        <![CDATA[
+          return Array.prototype.indexOf.call(this.children, item);
+        ]]>
+        </body>
+      </method>
+
+      <method name="getItemAtIndex">
+        <parameter name="index"/>
+        <body>
+        <![CDATA[
+          return this.children.item(index);
+        ]]>
+        </body>
+      </method>
+
+      <method name="_selectNewTab">
+        <parameter name="aNewTab"/>
+        <parameter name="aFallbackDir"/>
+        <parameter name="aWrap"/>
+        <body>
+        <![CDATA[
+          this.ariaFocusedItem = null;
+
+          var requestedTab = aNewTab;
+          while (aNewTab.hidden || aNewTab.disabled || !this._canAdvanceToTab(aNewTab)) {
+            aNewTab = aFallbackDir == -1 ? aNewTab.previousElementSibling : aNewTab.nextElementSibling;
+            if (!aNewTab && aWrap)
+              aNewTab = aFallbackDir == -1 ? this.children[this.children.length - 1] :
+                                             this.children[0];
+            if (!aNewTab || aNewTab == requestedTab)
+              return;
+          }
+
+          var isTabFocused = false;
+          try {
+            isTabFocused =
+              (document.commandDispatcher.focusedElement == this.selectedItem);
+          } catch (e) {}
+          this.selectedItem = aNewTab;
+          if (isTabFocused) {
+            aNewTab.focus();
+          } else if (this.getAttribute("setfocus") != "false") {
+            let selectedPanel = this.tabbox.selectedPanel;
+            document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel);
+
+            // Make sure that the focus doesn't move outside the tabbox
+            if (this.tabbox) {
+              try {
+                let el = document.commandDispatcher.focusedElement;
+                while (el && el != this.tabbox.tabpanels) {
+                  if (el == this.tabbox || el == selectedPanel)
+                    return;
+                  el = el.parentNode;
+                }
+                aNewTab.focus();
+              } catch (e) {
+              }
+            }
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="_canAdvanceToTab">
+        <parameter name="aTab"/>
+        <body>
+        <![CDATA[
+          return true;
+        ]]>
+        </body>
+      </method>
+
+      <method name="advanceSelectedTab">
+        <parameter name="aDir"/>
+        <parameter name="aWrap"/>
+        <body>
+        <![CDATA[
+          var startTab = this.ariaFocusedItem || this.selectedItem;
+          var next = startTab[(aDir == -1 ? "previous" : "next") + "ElementSibling"];
+          if (!next && aWrap) {
+            next = aDir == -1 ? this.children[this.children.length - 1] :
+                                this.children[0];
+          }
+          if (next && next != startTab) {
+            this._selectNewTab(next, aDir, aWrap);
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="appendItem">
+        <parameter name="label"/>
+        <parameter name="value"/>
+        <body>
+        <![CDATA[
+          var tab = document.createXULElement("tab");
+          tab.setAttribute("label", label);
+          tab.setAttribute("value", value);
+          this.appendChild(tab);
+          return tab;
+        ]]>
+        </body>
+      </method>
+    </implementation>
+
+    <handlers>
+      <handler event="DOMMouseScroll">
+      <![CDATA[
+        if (this._prefService.getBoolPref("toolkit.tabbox.switchByScrolling")) {
+          if (event.detail > 0) {
+            this.advanceSelectedTab(1, false);
+          } else {
+            this.advanceSelectedTab(-1, false);
+          }
+          event.stopPropagation();
+        }
+      ]]>
+      </handler>
+    </handlers>
+  </binding>
+</bindings>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -403,16 +403,17 @@ stack {
 
 /********** tabbox *********/
 
 tabbox {
   -moz-box-orient: vertical;
 }
 
 tabs {
+  -moz-binding: url("chrome://global/content/bindings/tabbox.xml#tabs");
   -moz-box-orient: horizontal;
 }
 
 tab {
   -moz-box-align: center;
   -moz-box-pack: center;
 }