diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js
index 0f5ae08..bfd5626 100644
--- a/core/modules/outside_in/js/offcanvas.js
+++ b/core/modules/outside_in/js/offcanvas.js
@@ -7,77 +7,61 @@
 
   'use strict';
 
-  // Set the initial state of the off-canvas element.
-  // If the state has been set previously, use it.
-  Drupal.offCanvas = {
-    visible: (Drupal.offCanvas ? Drupal.offCanvas.visible : false)
-  };
-
   /**
-   * Create a wrapper container for the off-canvas element.
+   * Off-canvas namespace.
    *
-   * @return {jQuery}
-   *   jQuery object that is the off-canvas wrapper element.
+   * @namespace
    */
-  Drupal.theme.createOffCanvasWrapper = function createOffCanvasWrapper() {
-    return $('<div id="offcanvas" ' + (document.dir === 'ltr' ? 'data-offset-right' : 'data-offset-left') + ' role="region" aria-labelledby="offcanvas-header"></div>');
+  Drupal.offCanvas = {
+
+    /**
+     * State of the off-canvas element.
+     *
+     * @type {bool}
+     */
+    visible: false
   };
 
   /**
-   * Create the title element for the off-canvas element.
+   * Create a wrapper container for the off-canvas element.
    *
    * @param {string} title
    *   The title string.
+   * @param {string} html
+   *   This is fully rendered HTML from Drupal.
    *
-   * @return {object}
-   *   jQuery object that is the off-canvas title element.
+   * @return {string}
+   *   Off-canvas wrapper element.
    */
-  Drupal.theme.createTitle = function createTitle(title) {
-    return $('<h1 id="offcanvas-header">' + title + '</h1>');
+  Drupal.theme.createOffCanvasWrapper = function (title, html) {
+    return '<div data-drupal-canvas="canvas-wrapper" id="offcanvas" ' + (document.dir === 'ltr' ? 'data-offset-right' : 'data-offset-left') + ' role="region" aria-labelledby="offcanvas-header">' +
+        '<h1 id="offcanvas-header">' + title + '</h1>' +
+        Drupal.theme('createOffCanvasClose') +
+        Drupal.theme('createOffCanvasContent', html) +
+      '</div>';
   };
 
   /**
    * Create the actual off-canvas content.
    *
-   * @param {string} data
+   * @param {string} html
    *   This is fully rendered HTML from Drupal.
    *
-   * @return {object}
-   *   jQuery object that is the off-canvas content element.
+   * @return {string}
+   *   Off-canvas content element.
    */
-  Drupal.theme.createOffCanvasContent = function createOffCanvasContent(data) {
-    return $('<div class="offcanvas-content">' + data + '</div>');
+  Drupal.theme.createOffCanvasContent = function (html) {
+    return '<div data-drupal-canvas="content" class="offcanvas-content">' + html + '</div>';
   };
 
   /**
    * Create the off-canvas close element.
    *
-   * @param {object} offCanvasWrapper
-   *   The jQuery off-canvas wrapper element
-   * @param {object} pageWrapper
-   *   The jQuery off page wrapper element
-   *
-   * @return {jQuery}
-   *   jQuery object that is the off-canvas close element.
+   * @return {string}
+   *   Off-canvas close element.
    */
-  Drupal.theme.createOffCanvasClose = function createOffCanvasClose(offCanvasWrapper, pageWrapper) {
-    return $([
-      '<button class="offcanvasClose" aria-label="',
-      Drupal.t('Close configuration tray.'),
-      '"><span class="visually-hidden">',
-      Drupal.t('Close'),
-      '</span></button>'
-    ].join(''))
-    .on('click', function () {
-      pageWrapper
-        .removeClass('js-tray-open')
-        .one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function () {
-          Drupal.offCanvas.visible = false;
-          offCanvasWrapper.remove();
-          Drupal.announce(Drupal.t('Configuration tray closed.'));
-        }
-      );
-    });
+  Drupal.theme.createOffCanvasClose = function () {
+    return '<button data-drupal-canvas="close" type="button" class="offcanvasClose" aria-label="' + Drupal.t('Close configuration tray.') + '"><span class="visually-hidden">' + Drupal.t('Close') + '</span></button>';
   };
 
 
@@ -87,7 +71,7 @@
    * @param {Drupal.Ajax} ajax
    *   The Drupal Ajax object.
    * @param {object} response
-   *   Object holding the server response.
+   *   The response from the Ajax request.
    * @param {number} [status]
    *   The HTTP status code.
    */
@@ -95,37 +79,61 @@
     // Discover display/viewport size.
     // @todo Work on breakpoints for tray size:
     //   https://www.drupal.org/node/2784599.
-    var $pageWrapper = $('#main-canvas-wrapper');
-    // var pageWidth = $pageWrapper.width();
-
-    // Construct off-canvas wrapper
-    var $offcanvasWrapper = Drupal.theme('createOffCanvasWrapper');
-
-    // Construct off-canvas internal elements.
-    var $offcanvasClose = Drupal.theme('createOffCanvasClose', $offcanvasWrapper, $pageWrapper);
-    var $title = Drupal.theme('createTitle', response.dialogOptions.title);
-    var $offcanvasContent = Drupal.theme('createOffCanvasContent', response.data);
 
     // Put everything together.
-    $offcanvasWrapper.append([$offcanvasClose, $title, $offcanvasContent]);
+    ajax.commands.insert(ajax, {
+      selector: '[data-drupal-outsidein="main-canvas-wrapper"]',
+      data: Drupal.theme('createOffCanvasWrapper', response.dialogOptions.title, response.data),
+      method: 'append'
+    }, status);
+
+    $('[data-drupal-outsidein="main-canvas-wrapper"]').addClass('js-tray-open');
 
     // Handle opening or updating tray with content.
-    var existingTray = false;
     if (Drupal.offCanvas.visible) {
-      // Remove previous content then append new content.
-      $pageWrapper.find('#offcanvas').remove();
-      existingTray = true;
-    }
-    $pageWrapper.addClass('js-tray-open');
-    Drupal.offCanvas.visible = true;
-    $pageWrapper.append($offcanvasWrapper);
-    if (existingTray) {
       Drupal.announce(Drupal.t('Configuration tray content has been updated.'));
     }
     else {
       Drupal.announce(Drupal.t('Configuration tray opened.'));
     }
-    Drupal.attachBehaviors(document.querySelector('#offcanvas'), drupalSettings);
+    Drupal.offCanvas.visible = true;
+  };
+
+  /**
+   * Behaviors for offcanvas elements.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Bind listeners to the close button of the offcanvas element.
+   * @prop {Drupal~behaviorDetach} detach
+   *   Remove close button event listeners.
+   */
+  Drupal.behaviors.offCanvasInit = {
+    attach: function (context, settings) {
+      $(context).find('[data-drupal-canvas="close"]')
+        .once('close-canvas')
+        .on('click.offcanvas', function () {
+          var $pageWrapper = $('[data-drupal-outsidein="main-canvas-wrapper"]');
+          var $offCanvasWrapper = $('[data-drupal-canvas="canvas-wrapper"]');
+
+          $pageWrapper
+            .removeClass('js-tray-open')
+            .on('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function () {
+              Drupal.offCanvas.visible = false;
+              Drupal.announce(Drupal.t('Configuration tray closed.'));
+              Drupal.detachBehaviors($offCanvasWrapper[0], settings, 'unload');
+              $offCanvasWrapper.remove();
+            });
+        });
+    },
+    detach: function (context, settings, trigger) {
+      if (trigger === 'unload') {
+        $(context).find('[data-drupal-canvas="close"]')
+          .removeOnce('close-canvas')
+          .off('.offcanvas');
+      }
+    }
   };
 
 })(jQuery, Drupal);
diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js
index f67fa9d..5cad041 100644
--- a/core/modules/outside_in/js/outside_in.js
+++ b/core/modules/outside_in/js/outside_in.js
@@ -7,32 +7,8 @@
 
   'use strict';
 
-  $('.outside-in-editable')
-    // Bind an event listener to the .outside-in-editable div
-    // This listen for click events and stops default actions of those elements.
-    .on('click', '.js-outside-in-edit-mode', function (e) {
-      if (localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false') {
-        e.preventDefault();
-      }
-    })
-    // Bind an event listener to the .outside-in-editable div
-    // When a click occurs try and find the outside-in edit link
-    // and click it.
-    .not('div.contextual a, div.contextual button')
-    .on('click', function (e) {
-      if ($(e.target.offsetParent).hasClass('contextual')) {
-        return;
-      }
-      if (!localStorage.getItem('Drupal.contextualToolbar.isViewing')) {
-        return;
-      }
-      var editLink = $(e.target).find('li.outside-inblock-configure a')[0];
-      if (!editLink) {
-        var closest = $(e.target).closest('.outside-in-editable');
-        editLink = closest.find('li.outside-inblock-configure a')[0];
-      }
-      editLink.click();
-    });
+  var blockConfigureSelector = '[data-dialog-type="offcanvas"]';
+  var toggleEditSelector = '[data-drupal-outsidein="toggle"]';
 
   /**
    * Reacts to contextual links being added.
@@ -45,71 +21,70 @@
    * @listens event:drupalContextualLinkAdded
    */
   $(document).on('drupalContextualLinkAdded', function (event, data) {
-    // Bind Ajax behaviors to all items showing the class.
-    // @todo Fix contextual links to work with use-ajax links in
-    //    https://www.drupal.org/node/2764931.
-    data.$el.find('.use-ajax').once('ajax').each(function () {
-      // Below is copied directly from ajax.js to keep behavior the same.
-      var element_settings = {};
-      // Clicked links look better with the throbber than the progress bar.
-      element_settings.progress = {type: 'throbber'};
-
-      // For anchor tags, these will go to the target of the anchor rather
-      // than the usual location.
-      var href = $(this).attr('href');
-      if (href) {
-        element_settings.url = href;
-        element_settings.event = 'click';
-      }
-      element_settings.dialogType = $(this).data('dialog-type');
-      element_settings.dialog = $(this).data('dialog-options');
-      element_settings.base = $(this).attr('id');
-      element_settings.element = this;
-      Drupal.ajax(element_settings);
-    });
-
     // Bind a listener to all 'Quick edit' links for blocks
     // Click "Edit" button in toolbar to force Contextual Edit which starts
     // Outside-In edit mode also.
-    data.$el.find('.outside-inblock-configure a').on('click', function () {
-      if (!isActiveMode()) {
-        $('div.contextual-toolbar-tab.toolbar-tab button').click();
-      }
-    });
+    data.$el.find(blockConfigureSelector)
+      .on('click.outsidein', function () {
+        if (!isActiveMode()) {
+          $(toggleEditSelector).trigger('click.outsidein');
+        }
+      });
   });
 
   /**
    * Gets all items that should be toggled with class during edit mode.
    *
-   * @return {*}
+   * @return {jQuery}
    *   Items that should be toggled.
    */
-  var getItemsToToggle = function () {
-    return $('#main-canvas, #toolbar-bar, .outside-in-editable a, .outside-in-editable button')
-      .not('div.contextual a, div.contextual button');
-  };
+  function getItemsToToggle() {
+    return $('#main-canvas, #toolbar-bar, [data-drupal-outsidein="editable"] a, [data-drupal-outsidein="editable"] button')
+      .not('[data-contextual-id] a, [data-contextual-id] button');
+  }
 
-  var isActiveMode = function () {
+  /**
+   * Helper to check the state of the outside-in mode.
+   *
+   * @todo don't use a class for this.
+   *
+   * @return {bool}
+   *  State of the outside-in edit mode.
+   */
+  function isActiveMode() {
     return $('#toolbar-bar').hasClass('js-outside-in-edit-mode');
-  };
+  }
+
+  function toggleActiveMode() {
+    setActiveMode(!isActiveMode());
+  }
 
-  var setToggleActiveMode = function setToggleActiveMode(forceActive) {
-    forceActive = forceActive || false;
-    if (forceActive || !isActiveMode()) {
-      $('#toolbar-bar .contextual-toolbar-tab button').text(Drupal.t('Editing'));
+  /**
+   *  Helper to switch edit mode state.
+   *
+   * @param {bool} activeMode
+   *  True enable edit mode, false disable edit mode.
+   */
+  function setActiveMode(activeMode) {
+    activeMode = !!activeMode;
+    var $editButton = $(toggleEditSelector);
+    // Active edit mode.
+    if (activeMode) {
+      $editButton.text(Drupal.t('Editing'));
       // Close the Manage tray if open when entering edit mode.
       if ($('#toolbar-item-administration-tray').hasClass('is-active')) {
-        $('#toolbar-item-administration').click();
+        $('#toolbar-item-administration').trigger('click');
       }
-      getItemsToToggle().addClass('js-outside-in-edit-mode');
-      $('.edit-mode-inactive').addClass('visually-hidden');
     }
+    // Disable edit mode.
     else {
-      $('#toolbar-bar .contextual-toolbar-tab button').text(Drupal.t('Edit'));
-      getItemsToToggle().removeClass('js-outside-in-edit-mode');
-      $('.edit-mode-inactive').removeClass('visually-hidden');
+      $editButton.text(Drupal.t('Edit'));
+      // Close/remove offcanvas.
+      $('[data-drupal-canvas="close"]').trigger('click.offcanvas');
     }
-  };
+    getItemsToToggle().toggleClass('js-outside-in-edit-mode', activeMode);
+    $('.edit-mode-inactive').toggleClass('visually-hidden', activeMode);
+  }
 
   /**
    * Attaches contextual's edit toolbar tab behavior.
@@ -123,7 +98,7 @@
     attach: function () {
       var editMode = localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false';
       if (editMode) {
-        setToggleActiveMode(true);
+        setActiveMode(true);
       }
     }
   };
@@ -138,10 +113,33 @@
    */
   Drupal.behaviors.toggleActiveMode = {
     attach: function () {
-      $('.contextual-toolbar-tab.toolbar-tab button').on('click', function () {
-        setToggleActiveMode();
+
+      $(toggleEditSelector).once('outsidein').on('click.outsidein', toggleActiveMode);
+
+      var $editables = $('[data-drupal-outsidein="editable"]').once('outsidein');
+
+      // Bind an event listener to the editable div and stops default actions
+      // of those elements.
+      $editables.on('click.outsidein', '.js-outside-in-edit-mode', function (e) {
+        if (isActiveMode()) {
+          e.preventDefault();
+        }
       });
+
+      // When a click occurs try and find the outside-in edit link
+      // and click it.
+      $editables
+        .not('[data-contextual-id] a, [data-contextual-id] button')
+        .on('click.outsidein', function (e) {
+          // @todo check if we can't use currentTarget too.
+          if ($(e.target).parent().hasClass('contextual') || !localStorage.getItem('Drupal.contextualToolbar.isViewing')) {
+            return;
+          }
+
+          $(e.currentTarget).find(blockConfigureSelector).trigger('click');
+        });
     }
+    // @todo detach function.
   };
 
 })(jQuery, Drupal);
diff --git a/core/modules/outside_in/outside_in.module b/core/modules/outside_in/outside_in.module
index d7aa0ce..915a5fc 100644
--- a/core/modules/outside_in/outside_in.module
+++ b/core/modules/outside_in/outside_in.module
@@ -57,7 +57,7 @@ function outside_in_block_view_alter(array &$build) {
 function outside_in_page_top(array &$page_top) {
   // Opens a div for consistent wrapping to {{ page }} render in all themes.
   $page_top['outside_in_tray_open'] = [
-    '#markup' => '<div id="main-canvas-wrapper"><div id="main-canvas">',
+    '#markup' => '<div data-drupal-outsidein="main-canvas-wrapper" id="main-canvas-wrapper"><div data-drupal-outsidein="main-canvas" id="main-canvas">',
     '#weight' => 1000,
   ];
 }
@@ -90,8 +90,9 @@ function outside_in_preprocess_block(&$variables) {
   // The main system block does not contain the block contextual links.
   $variables['#cache']['contexts'][] = 'outside_in_is_applied';
   if ($variables['plugin_id'] !== 'system_main_block' && \Drupal::service('outside_in.manager')->isApplicable()) {
-    // Add class to all blocks to allow Javascript to target.
+    // Add class and attributes to all blocks to allow Javascript to target.
     $variables['attributes']['class'][] = 'outside-in-editable';
+    $variables['attributes']['data-drupal-outsidein'] = 'editable';
   }
 }
 
@@ -105,6 +106,7 @@ function outside_in_toolbar_alter(&$items) {
   if (isset($items['contextual']['tab']) && \Drupal::service('outside_in.manager')->isApplicable()) {
     $items['contextual']['#weight'] = -1000;
     $items['contextual']['#attached']['library'][] = 'outside_in/drupal.outside_in';
+    $items['contextual']['tab']['#attributes']['data-drupal-outsidein'] = 'toggle';
 
     // Set a class on items to mark whether they should be active in edit mode.
     // @todo Create a dynamic method for modules to set their own items.
