diff --git a/core/modules/contextual/contextual.base.css b/core/modules/contextual/contextual.base.css
index 8452f52..4d8d908 100644
--- a/core/modules/contextual/contextual.base.css
+++ b/core/modules/contextual/contextual.base.css
@@ -1,4 +1,3 @@
-
 /**
  * @file
  * Generic base styles for contextual module.
@@ -7,33 +6,13 @@
 .contextual-region {
   position: relative;
 }
-.touch .contextual .trigger {
-  display: block;
+.contextual .trigger:focus {
+  /* Override the .element-focusable position: static */
+  position: relative !important;
 }
-.contextual .contextual-links {
+.contextual-links {
   display: none;
 }
-.contextual-links-active .contextual-links {
+.contextual.open .contextual-links {
   display: block;
 }
-
-/**
- * The .element-focusable class extends the .element-invisible class to allow
- * the element to be focusable when navigated to via the keyboard.
- *
- * Add support for hover.
- */
-.touch .contextual-region .element-invisible.element-focusable,
-.contextual-region:hover .element-invisible.element-focusable  {
-  clip: auto;
-  overflow: visible;
-  height: auto;
-}
-/* Override the position for contextual links. */
-.contextual-region .element-invisible.element-focusable:active,
-.contextual-region .element-invisible.element-focusable:focus,
-.contextual-region:hover .element-invisible.element-focusable,
-.contextual-region-active .element-invisible.element-focusable,
-.touch .contextual-region .element-invisible.element-focusable  {
-  position: relative !important;
-}
diff --git a/core/modules/contextual/contextual.js b/core/modules/contextual/contextual.js
index ea51af2..a7f22e6 100644
--- a/core/modules/contextual/contextual.js
+++ b/core/modules/contextual/contextual.js
@@ -3,227 +3,395 @@
  * Attaches behaviors for the Contextual module.
  */
 
-(function ($, Drupal) {
+(function ($, Drupal, drupalSettings, Backbone, Modernizr) {
 
 "use strict";
 
-var contextuals = [];
+var options = $.extend({
+  strings: {
+    open: Drupal.t('open'),
+    close: Drupal.t('close')
+  }
+}, drupalSettings.contextual);
 
 /**
- * Attaches outline behavior for regions associated with contextual links.
+ * Determines if a contextual link is nested & overlapping, if so: adjusts it.
+ *
+ * This only deals with two levels of nesting; deeper levels are not touched.
+ *
+ * @param DOM contextual
+ *   A contextual link DOM element.
  */
-Drupal.behaviors.contextual = {
-  attach: function (context) {
-    var that = this;
-    $('ul.contextual-links', context).once('contextual', function () {
-      var $this = $(this);
-      var contextual = new Drupal.contextual($this, $this.closest('.contextual-region'));
-      contextuals.push(contextual);
-      $this.data('drupal-contextual', contextual);
-      that._adjustIfNestedAndOverlapping(this);
-    });
+function adjustIfNestedAndOverlapping (contextual) {
+  var $contextuals = $(contextual)
+    // @todo confirm that .closest() is not sufficient
+    .parents('.contextual-region').eq(-1)
+    .find('.contextual');
 
-    // Bind to edit mode changes.
-    $('body').once('contextual', function () {
-      $(document).on('drupalEditModeChanged.contextual', toggleEditMode);
-    });
-  },
+  // Early-return when there's no nesting.
+  if ($contextuals.length === 1) {
+    return;
+  }
 
-  /**
-   * Determines if a contextual link is nested & overlapping, if so: adjusts it.
-   *
-   * This only deals with two levels of nesting; deeper levels are not touched.
-   *
-   * @param DOM contextualLink
-   *   A contextual link DOM element.
-   */
-  _adjustIfNestedAndOverlapping: function (contextualLink) {
-    var $contextuals = $(contextualLink)
-      .parents('.contextual-region').eq(-1)
-      .find('.contextual');
-
-    // Early-return when there's no nesting.
-    if ($contextuals.length === 1) {
-      return;
-    }
+  // If the two contextual links overlap, then we move the second one.
+  var firstTop = $contextuals.eq(0).offset().top;
+  var secondTop = $contextuals.eq(1).offset().top;
+  if (firstTop === secondTop) {
+    var $nestedContextual = $contextuals.eq(1);
 
-    // If the two contextual links overlap, then we move the second one.
-    var firstTop = $contextuals.eq(0).offset().top;
-    var secondTop = $contextuals.eq(1).offset().top;
-    if (firstTop === secondTop) {
-      var $nestedContextual = $contextuals.eq(1);
-
-      // Retrieve height of nested contextual link.
-      var height = 0;
-      var $trigger = $nestedContextual.find('.trigger');
-      // Elements with the .element-invisible class have no dimensions, so this
-      // class must be temporarily removed to the calculate the height.
-      $trigger.removeClass('element-invisible');
-      height = $nestedContextual.height();
-      $trigger.addClass('element-invisible');
-
-      // Adjust nested contextual link's position.
-      $nestedContextual.css({ top: $nestedContextual.position().top + height });
-    }
+    // Retrieve height of nested contextual link.
+    var height = 0;
+    var $trigger = $nestedContextual.find('.trigger');
+    // Elements with the .element-invisible class have no dimensions, so this
+    // class must be temporarily removed to the calculate the height.
+    $trigger.removeClass('element-invisible');
+    height = $nestedContextual.height();
+    $trigger.addClass('element-invisible');
+
+    // Adjust nested contextual link's position.
+    $nestedContextual.css({ top: $nestedContextual.position().top + height });
   }
-};
+}
 
 /**
- * Contextual links object.
+ * Shows or hides all contextual triggers.
+ *
+ * @param jQuery.Event event
+ *
+ * @param Object data
+ *   An object with the following properties:
+ *   - status: Represents the state of the global edit mode. True means edit
+ *   mode is enabled. In this case, the contextual models should be locked
+ *   so that their triggers are available.
  */
-Drupal.contextual = function($links, $region) {
-  this.$links = $links;
-  this.$region = $region;
-
-  this.init();
-};
+function toggleEditMode (event, data) {
+  Drupal.contextual.collection.each(function (model) {
+    model.set('isLocked', data.status);
+  });
+}
 
 /**
- * Initiates a contextual links object.
+ * Generate markup required for contextual links
+ *
+ * @param link
  */
-Drupal.contextual.prototype.init = function() {
-  // Wrap the links to provide positioning and behavior attachment context.
-  this.$wrapper = $(Drupal.theme.contextualWrapper())
-    .insertBefore(this.$links)
-    .append(this.$links);
-
-  // Mark the links as hidden. Use aria-role form so that the number of items
-  // in the list is spoken.
-  this.$links
-    .prop('hidden', true)
-    .attr('role', 'form');
-
-  // Create and append the contextual links trigger.
-  var action = Drupal.t('Open');
-
-  var parentBlock = this.$region.find('h2').first().text();
-  this.$trigger = $(Drupal.theme.contextualTrigger())
-    .text(Drupal.t('@action @parent configuration options', {'@action': action, '@parent': parentBlock}))
-    // Set the aria-pressed state.
-    .prop('aria-pressed', false)
-    .prependTo(this.$wrapper);
-
-  // The trigger behaviors are never detached or mutated.
-  this.$region
-    .on('click.contextual', '.contextual .trigger:first', $.proxy(this.triggerClickHandler, this))
-    .on('mouseleave.contextual', '.contextual', {show: false}, $.proxy(this.triggerLeaveHandler, this));
-  // Attach highlight behaviors.
-  this.attachHighlightBehaviors();
-};
+function initContextual (index, links) {
+  var $links = $(links);
+  var $region = $links.closest('.contextual-region');
+  var contextual = Drupal.contextual;
 
-/**
- * Attaches highlight-on-mouseenter behaviors.
- */
-Drupal.contextual.prototype.attachHighlightBehaviors = function () {
-  // Bind behaviors through delegation.
-  var highlightRegion = $.proxy(this.highlightRegion, this);
-  this.$region
-    .on('mouseenter.contextual.highlight', {highlight: true}, highlightRegion)
-    .on('mouseleave.contextual.highlight', {highlight: false}, highlightRegion)
-    .on('click.contextual.highlight', '.contextual-links a', {highlight: false}, highlightRegion)
-    .on('focus.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: true}, highlightRegion)
-    .on('blur.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: false}, highlightRegion);
-};
+  // Create a contextual links wrapper to provide positioning and behavior
+  // attachment context.
+  var $wrapper = $(Drupal.theme('contextualWrapper'))
+    .insertBefore($links)
+    // In the wrapper, first add the trigger element.
+    .append(Drupal.theme('contextualTrigger'))
+    // In the wrapper, then add the contextual links.
+    .append($links);
+
+  // Create a model, add it to the collection.
+  var model = new contextual.Model({
+    title: $region.find('h2:first').text().trim()
+  });
+  contextual.collection.add(model);
+
+  // Create the appropriate views for this model.
+  var viewOptions = $.extend({ el: $wrapper, model: model }, options);
+  contextual.views.push({
+    visual: new contextual.VisualView(viewOptions),
+    aural: new contextual.AuralView(viewOptions),
+    keyboard: new contextual.KeyboardView(viewOptions)
+  });
+  contextual.regionViews.push(new contextual.RegionView(
+    $.extend({ el: $region, model: model }, options))
+  );
+
+  // Let other JavaScript react to the adding of a new contextual link.
+  $(document).trigger('drupalContextualLinkAdded', {
+    $el: $links,
+    $region: $region,
+    model: model
+  });
+
+  // Fix visual collisions between contextual link triggers.
+  adjustIfNestedAndOverlapping(links);
+}
 
-/**
- * Detaches unhighlight-on-mouseleave behaviors.
- */
-Drupal.contextual.prototype.detachHighlightBehaviors = function () {
-  this.$region.off('.contextual.highlight');
-};
 
 /**
- * Toggles the highlighting of a contextual region.
+ * Attaches outline behavior for regions associated with contextual links.
  *
- * @param {Object} event
- *   jQuery Event object.
+ * Events
+ *   Contextual triggers an event that can be used by other scripts.
+ *   - drupalContextualLinkAdded: Triggered when a contextual link is added.
  */
-Drupal.contextual.prototype.highlightRegion = function(event) {
-  // Set up a timeout to delay the dismissal of the region highlight state.
-  if (!event.data.highlight && this.timer === undefined) {
-    return this.timer = window.setTimeout($.proxy($.fn.trigger, $(event.target), 'mouseleave.contextual'), 100);
-  }
-  // Clear the timeout to prevent an infinite loop of mouseleave being
-  // triggered.
-  if (this.timer) {
-    window.clearTimeout(this.timer);
-    delete this.timer;
-  }
-  // Toggle active state of the contextual region based on the highlight value.
-  this.$region.toggleClass('contextual-region-active', event.data.highlight);
-  // Hide the links if the contextual region is inactive.
-  var state = this.$region.hasClass('contextual-region-active');
-  if (!state) {
-    this.showLinks(state);
+Drupal.behaviors.contextual = {
+  attach: function (context) {
+    // Create a model, and corresponding views for each of the contextual links.
+    $(context).find('.contextual-links').once('contextual').each(initContextual);
+
+    // Bind to edit mode changes.
+    if ($('body').once('contextual').length) {
+      $(document).on('drupalEditModeChanged.contextual', toggleEditMode);
+    }
   }
 };
 
 /**
- * Handles click on the contextual links trigger.
- *
- * @param {Object} event
- *   jQuery Event object.
+ * Model and View definitions.
  */
-Drupal.contextual.prototype.triggerClickHandler = function (event) {
-  event.preventDefault();
-  // Hide all nested contextual triggers while the links are shown for this one.
-  this.$region.find('.contextual .trigger:not(:first)').hide();
-  this.showLinks();
-};
+Drupal.contextual = {
+  // The Drupal.contextual.View instances associated with each list element of
+  // contextual links.
+  views: [],
 
-/**
- * Handles mouseleave on the contextual links trigger.
- *
- * @param {Object} event
- *   jQuery Event object.
- */
-Drupal.contextual.prototype.triggerLeaveHandler = function (event) {
-  var show = event && event.data && event.data.show;
-  // Show all nested contextual triggers when the links are hidden for this one.
-  this.$region.find('.contextual .trigger:not(:first)').show();
-  this.showLinks(show);
-};
+  // The Drupal.contextual.RegionView instances associated with each contextual
+  // region element.
+  regionViews: [],
 
-/**
- * Toggles the active state of the contextual links.
- *
- * @param {Boolean} show
- *   (optional) True if the links should be shown. False is the links should be
- *   hidden.
- */
-Drupal.contextual.prototype.showLinks = function(show) {
-  this.$wrapper.toggleClass('contextual-links-active', show);
-  var isOpen = this.$wrapper.hasClass('contextual-links-active');
-  var action = (isOpen) ? Drupal.t('Close') : Drupal.t('Open');
-  var parentBlock = this.$region.find('h2').first().text();
-  this.$trigger
-    .text(Drupal.t('@action @parent configuration options', {'@action': action, '@parent': parentBlock}))
-    // Set the aria-pressed state.
-    .prop('aria-pressed', isOpen);
-  // Mark the links as hidden if they are.
-  if (isOpen) {
-    this.$links.prop('hidden', false);
-  }
-  else {
-    this.$links.prop('hidden', true);
-  }
+  /**
+   * Models the state of a contextual link's trigger and list.
+   */
+  Model: Backbone.Model.extend({
+    defaults: {
+      // The title of the entity to which these contextual links apply.
+      title: '',
+      // Represents if the contextual region is being hovered.
+      regionIsHovered: false,
+      // Represents if the contextual trigger or options have focus.
+      hasFocus: false,
+      // Represents if the contextual options for an entity are available to
+      // be selected.
+      isOpen: false,
+      // When the model is locked, the trigger remains active.
+      isLocked: false
+    },
+
+    /**
+     * Opens or closes the contextual link.
+     *
+     * If it is opened, then also give focus.
+     */
+    toggleOpen: function () {
+      var newIsOpen = !this.get('isOpen');
+      this.set('isOpen', newIsOpen);
+      if (newIsOpen) {
+        this.focus();
+      }
+      return this;
+    },
+
+    /**
+     * Closes this contextual link.
+     *
+     * Does not call blur() because we want to allow a contextual link to have
+     * focus, yet be closed for example when hovering.
+     */
+    close: function () {
+      this.set('isOpen', false);
+      return this;
+    },
+
+    /**
+     * Gives focus to this contextual link.
+     *
+     * Also closes + removes focus from every other contextual link.
+     */
+    focus: function () {
+      this.set('hasFocus', true);
+      var cid = this.cid;
+      this.collection.each(function (model) {
+        if (model.cid !== cid) {
+          model.close().blur();
+        }
+      });
+      return this;
+    },
+
+    /**
+     * Removes focus from this contextual link, unless it is open.
+     */
+    blur: function () {
+      if (!this.get('isOpen')) {
+        this.set('hasFocus', false);
+      }
+      return this;
+    }
+  }),
+
+  /**
+   * Renders the visual view of a contextual link. Listens to mouse & touch.
+   */
+  VisualView: Backbone.View.extend({
+    events: function () {
+      // Prevents delay and simulated mouse events.
+      var touchEndToClick = function (event) {
+        event.preventDefault();
+        event.target.click();
+      };
+      var mapping = {
+        'click .trigger': function () { this.model.toggleOpen(); },
+        'touchend .trigger': touchEndToClick,
+        'click .contextual-links a': function () { this.model.close().blur(); },
+        'touchend .contextual-links a': touchEndToClick
+      };
+      // We only want mouse hover events on non-touch.
+      if (!Modernizr.touch) {
+        mapping.mouseenter =  function () { this.model.focus(); };
+      }
+      return mapping;
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.model.on('change', this.render, this);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      var isOpen = this.model.get('isOpen');
+      // The trigger should be visible when:
+      //  - the mouse hovered over the region,
+      //  - the trigger is locked,
+      //  - and for as long as the contextual menu is open.
+      var isVisible = this.model.get('isLocked') || this.model.get('regionIsHovered') || isOpen;
+
+      this.$el
+        // The open state determines if the links are visible.
+        .toggleClass('open', isOpen)
+        // Update the visibility of the trigger.
+        .find('.trigger').toggleClass('element-invisible', !isVisible);
+
+      // Nested contextual region handling: hide any nested contextual triggers.
+      if ('isOpen' in this.model.changed) {
+        this.$el.closest('.contextual-region')
+          .find('.contextual .trigger:not(:first)')
+          .toggle(!isOpen);
+      }
+
+      return this;
+    }
+  }),
+
+  /**
+   * Renders the aural view of a contextual link (i.e.screen reader support).
+   */
+  AuralView: Backbone.View.extend({
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function (options) {
+      this.model.on('change', this.render, this);
+
+      // Use aria-role form so that the number of items in the list is spoken.
+      this.$el.attr('role', 'form');
+
+      // Initial render.
+      this.render();
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      var isOpen = this.model.get('isOpen');
+
+      // Set the hidden property of the links.
+      this.$el.find('.contextual-links')
+        .prop('hidden', !isOpen);
+
+      // Update the view of the trigger.
+      this.$el.find('.trigger')
+        .text(Drupal.t('@action @title configuration options', {
+          '@action': (!isOpen) ? this.options.strings.open : this.options.strings.close,
+          '@title': this.model.get('title')
+        }))
+        .attr('aria-pressed', isOpen);
+    }
+  }),
+
+  /**
+   * Listens to keyboard.
+   */
+  KeyboardView: Backbone.View.extend({
+    events: {
+      'focus .trigger, focus .contextual-links a' : function () {
+        // Clear the timeout that might have been set by blurring a link.
+        window.clearTimeout(this.timer);
+        this.model.focus();
+      },
+      'blur .trigger': function () { this.model.blur(); },
+      'blur .contextual-links a': function () {
+        // Set up a timeout to allow a user to tab between the trigger and the
+        // contextual links without the menu dismissing.
+        var that = this;
+        this.timer = window.setTimeout(function () {
+          that.model.close().blur();
+        }, 150);
+      }
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      // The timer is used to create a delay before dismissing the contextual
+      // links on blur. This is only necessary when keyboard users tab into
+      // contextual links without edit mode (i.e. without TabbingManager).
+      // That means that if we decide to disable tabbing of contextual links
+      // without edit mode, all this timer logic can go away.
+      this.timer = NaN;
+    }
+  }),
 
+  /**
+   * Renders the visual view of a contextual region element.
+   */
+  RegionView: Backbone.View.extend({
+    events: function () {
+      var mapping = {
+        mouseenter: function () { this.model.set('regionIsHovered', true); },
+        mouseleave: function () {
+          this.model.close().blur().set('regionIsHovered', false);
+        }
+      };
+      // We don't want mouse hover events on touch.
+      if (Modernizr.touch) {
+        mapping = {};
+      }
+      return mapping;
+    },
+
+    /**
+     * Implements Backbone.View.prototype.initialize().
+     */
+    initialize: function () {
+      this.model.on('change:hasFocus', this.render, this);
+    },
+
+    /**
+     * Implements Backbone.View.prototype.render().
+     */
+    render: function () {
+      this.$el.toggleClass('focus', this.model.get('hasFocus'));
+
+      return this;
+    }
+  })
 };
 
-/**
- * Shows or hides all pencil icons and corresponding contextual regions.
- */
-function toggleEditMode (event, data) {
-  for (var i = contextuals.length - 1; i >= 0; i--) {
-    contextuals[i][(data.status) ? 'detachHighlightBehaviors' : 'attachHighlightBehaviors']();
-    contextuals[i].$region.toggleClass('contextual-region-active', data.status);
-  }
-}
+// A Backbone.Collection of Drupal.contextual.Model instances.
+Drupal.contextual.collection = new Backbone.Collection([], { model: Drupal.contextual.Model });
+
 
 /**
  * Wraps contextual links.
  *
- * @return {String}
+ * @return String
  *   A string representing a DOM fragment.
  */
 Drupal.theme.contextualWrapper = function () {
@@ -233,11 +401,11 @@ Drupal.theme.contextualWrapper = function () {
 /**
  * A trigger is an interactive element often bound to a click handler.
  *
- * @return {String}
+ * @return String
  *   A string representing a DOM fragment.
  */
 Drupal.theme.contextualTrigger = function () {
   return '<button class="trigger element-invisible element-focusable" type="button"></button>';
 };
 
-})(jQuery, Drupal);
+})(jQuery, Drupal, drupalSettings, Backbone, Modernizr);
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index 3ceda05..51747c6 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -91,6 +91,9 @@ function contextual_library_info() {
     'dependencies' => array(
       array('system', 'jquery'),
       array('system', 'drupal'),
+      array('system', 'drupalSettings'),
+      array('system', 'backbone'),
+      array('system', 'modernizr'),
       array('system', 'jquery.once'),
     ),
   );
@@ -107,8 +110,9 @@ function contextual_library_info() {
     ),
     'dependencies' => array(
       array('system', 'jquery'),
-      array('system', 'jquery.once'),
+      array('system', 'drupal'),
       array('system', 'backbone'),
+      array('system', 'jquery.once'),
       array('system', 'drupal.tabbingmanager'),
       array('system', 'drupal.announce'),
     ),
diff --git a/core/modules/contextual/contextual.theme-rtl.css b/core/modules/contextual/contextual.theme-rtl.css
index e04a21f..c001e31 100644
--- a/core/modules/contextual/contextual.theme-rtl.css
+++ b/core/modules/contextual/contextual.theme-rtl.css
@@ -16,15 +16,17 @@
  */
 .contextual .trigger {
   float: left;
-  right: 0;
+  right: auto;
   left: 2px;
 }
 
 /**
  * Contextual links.
  */
-.contextual .contextual-links {
+.contextual-region .contextual .contextual-links {
   border-radius: 0 4px 4px 4px;
   float: left;
+  left: 2px;
+  right: auto;
   text-align: right;
 }
diff --git a/core/modules/contextual/contextual.theme.css b/core/modules/contextual/contextual.theme.css
index 7c5cde4..fef8240 100644
--- a/core/modules/contextual/contextual.theme.css
+++ b/core/modules/contextual/contextual.theme.css
@@ -14,6 +14,14 @@
 }
 
 /**
+ * Contextual region.
+ */
+.contextual-region.focus {
+  outline: 1px dashed #d6d6d6;
+  outline-offset: 1px;
+}
+
+/**
  * Contextual trigger.
  */
 .contextual .trigger {
@@ -34,15 +42,16 @@
   padding: 0 2px;
   position: relative;
   right: 2px; /* LTR */
-  width: 28px;
+  /* Override the .element-focusable height: auto */
+  width: 28px !important;
   text-indent: -9999px;
-  z-index: 2;
   cursor: pointer;
 }
-.contextual-links-active .trigger {
+.contextual.open .trigger {
   border-bottom-color: transparent;
   border-radius: 13px 13px 0 0;
   box-shadow: none;
+  z-index: 2;
 }
 
 /**
@@ -59,10 +68,10 @@
   margin: 0;
   padding: 0.25em 0;
   position: relative;
+  right: 2px; /* LTR */
   text-align: left; /* LTR */
   top: -1px;
   white-space: nowrap;
-  z-index: 1;
 }
 .contextual-region .contextual .contextual-links li {
   background-color: #fff;
diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index c50da18..d3634dc 100644
--- a/core/modules/contextual/contextual.toolbar.js
+++ b/core/modules/contextual/contextual.toolbar.js
@@ -3,10 +3,67 @@
  * Attaches behaviors for the Contextual module's edit toolbar tab.
  */
 
-(function ($, Backbone, Drupal, document, localStorage) {
+(function ($, Drupal, Backbone) {
 
 "use strict";
 
+var options = {
+  strings: {
+    tabbingReleased: Drupal.t('Tabbing is no longer constrained by the Contextual module'),
+    tabbingConstrained: Drupal.t('Tabbing is constrained to a set of @contextualsCount and the Edit mode toggle'),
+    pressEsc: Drupal.t('Press the esc key to exit.'),
+    contextualsCount: {
+      singular: '@count contextual link',
+      plural: '@count contextual links'
+    }
+  }
+};
+
+function initContextualToolbar (context) {
+  var $contextuals = $(context).find('.contextual-links');
+  var contextualToolbar = Drupal.contextualToolbar;
+  var $tab = $('.js .toolbar .bar .contextual-toolbar-tab');
+  var model = new contextualToolbar.EditToggleModel({
+    isViewing: true,
+    contextuals: $contextuals.get()
+  });
+  contextualToolbar.collection.add(model);
+
+  contextualToolbar.views.push(new contextualToolbar.EditToggleView({
+    el: $tab,
+    model: model,
+    strings: options.strings
+  }));
+
+  // Update the model based on overlay events.
+  $(document).on({
+    'drupalOverlayOpen.contextualToolbar': function () {
+      model.set('isVisible', false);
+    },
+    'drupalOverlayClose.contextualToolbar': function () {
+      model.set('isVisible', true);
+    }
+  });
+
+  // Update the model to show the edit tab if there's >=1 contextual link.
+  if ($contextuals.length > 0) {
+    model.set('isVisible', true);
+  }
+
+  // Allow other scripts to respond to edit mode changes.
+  model.on('change:isViewing', function (model, value) {
+    $(document).trigger('drupalEditModeChanged', { status: !value });
+  });
+
+  // Checks whether localStorage indicates we should start in edit mode
+  // rather than view mode.
+  // @see Drupal.contextualToolbar.EditToggleView.persist()
+  if (localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false') {
+    model.set('isViewing', false);
+  }
+}
+
+
 /**
  * Attaches contextual's edit toolbar tab behavior.
  *
@@ -16,214 +73,167 @@
  */
 Drupal.behaviors.contextualToolbar = {
   attach: function (context) {
-    var that = this;
-    $('body').once('contextualToolbar-init', function () {
-      var options = $.extend({}, that.defaults);
-      var $contextuals = $(context).find('.contextual-links');
-      var $tab = $('.js .toolbar .bar .contextual-toolbar-tab');
-      var model = new Drupal.contextualToolbar.models.EditToggleModel({
-        isViewing: true,
-        contextuals: $contextuals.get()
-      });
-      var view = new Drupal.contextualToolbar.views.EditToggleView({
-        el: $tab,
-        model: model,
-        strings: options.strings
-      });
-
-      // Update the model based on overlay events.
-      $(document)
-        .on('drupalOverlayOpen.contextualToolbar', function () {
-          model.set('isVisible', false);
-        })
-        .on('drupalOverlayClose.contextualToolbar', function () {
-          model.set('isVisible', true);
-        });
-
-      // Update the model to show the edit tab if there's >=1 contextual link.
-      if ($contextuals.length > 0) {
-        model.set('isVisible', true);
-      }
-
-      // Allow other scripts to respond to edit mode changes.
-      model.on('change:isViewing', function (model, value) {
-        $(document).trigger('drupalEditModeChanged', { status: !value });
-      });
-
-      // Checks whether localStorage indicates we should start in edit mode
-      // rather than view mode.
-      // @see Drupal.contextualToolbar.views.EditToggleView.persist()
-      if (localStorage.getItem('Drupal.contextualToolbar.isViewing') !== null) {
-        model.set('isViewing', false);
-      }
-    });
-  },
-
-  defaults: {
-    strings: {
-      tabbingReleased: Drupal.t('Tabbing is no longer constrained by the Contextual module'),
-      tabbingConstrained: Drupal.t('Tabbing is constrained to a set of @contextualsCount and the Edit mode toggle'),
-      pressEsc: Drupal.t('Press the esc key to exit.'),
-      contextualsCount: {
-        singular: '@count contextual link',
-        plural: '@count contextual links'
-      }
+    if ($('body').once('contextualToolbar-init').length) {
+      initContextualToolbar(context);
     }
   }
 };
 
-Drupal.contextualToolbar = Drupal.contextualToolbar || { models: {}, views: {}};
-
-/**
- * Backbone Model for the edit toggle.
- */
-Drupal.contextualToolbar.models.EditToggleModel = Backbone.Model.extend({
-  defaults: {
-    // Indicates whether the toggle is currently in "view" or "edit" mode.
-    isViewing: true,
-    // Indicates whether the toggle should be visible or hidden.
-    isVisible: false,
-    // The set of elements that can be reached via the tab key when edit mode
-    // is enabled.
-    tabbingContext: null,
-    // The set of contextual links stored as an Array.
-    contextuals: []
-  }
-});
-
-/**
- * Handles edit mode toggle interactions.
- */
-Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({
-
-  events: { 'click': 'onClick' },
-
-  // Tracks whether the tabbing constraint announcement has been read once yet.
-  announcedOnce: false,
+Drupal.contextualToolbar = {
+  collection: null,
+  views: [],
 
   /**
-   * Implements Backbone Views' initialize().
+   * Backbone Model for the edit toggle.
    */
-  initialize: function () {
-
-    this.strings = this.options.strings;
-
-    this.model.on('change', this.render, this);
-    this.model.on('change:isViewing', this.persist, this);
-    this.model.on('change:isViewing', this.manageTabbing, this);
-
-    $(document)
-      .on('keyup', $.proxy(this.onKeypress, this));
-  },
-
-  /**
-   * Implements Backbone Views' render().
-   */
-  render: function () {
-    var args = arguments;
-    // Render the visibility.
-    this.$el.toggleClass('element-hidden', !this.model.get('isVisible'));
-
-    // Render the state.
-    var isViewing = this.model.get('isViewing');
-    this.$el.find('button')
-      .toggleClass('active', !isViewing)
-      .attr('aria-pressed', !isViewing);
-
-    return this;
-  },
-
-  /**
-   * Limits tabbing to the contextual links and edit mode toolbar tab.
-   *
-   * @param Drupal.contextualToolbar.models.EditToggleModel model
-   *   An EditToggleModel Backbone model.
-   * @param bool isViewing
-   *   The value of the isViewing attribute in the model.
-   */
-  manageTabbing: function (model, isViewing) {
-    var tabbingContext = this.model.get('tabbingContext');
-    // Always release an existing tabbing context.
-    if (tabbingContext) {
-      tabbingContext.release();
-    }
-    // Create a new tabbing context when edit mode is enabled.
-    if (!isViewing) {
-      tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual'));
-      this.model.set('tabbingContext', tabbingContext);
-    }
-  },
-
-  /**
-   * Model change handler; persists the isViewing value to localStorage.
-   *
-   * isViewing === true is the default, so only stores in localStorage when
-   * it's not the default value (i.e. false).
-   *
-   * @param Drupal.contextualToolbar.models.EditToggleModel model
-   *   An EditToggleModel Backbone model.
-   * @param bool isViewing
-   *   The value of the isViewing attribute in the model.
-   */
-  persist: function (model, isViewing) {
-    if (!isViewing) {
-      localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false');
-    }
-    else {
-      localStorage.removeItem('Drupal.contextualToolbar.isViewing');
-    }
-  },
-
-  /**
-   * Passes state update messsages to Drupal.announce.
-   */
-  announceTabbingConstraint: function () {
-    var isViewing = this.model.get('isViewing');
-
-    if (!isViewing) {
-      var contextuals = this.model.get('contextuals');
-      Drupal.announce(Drupal.t(this.strings.tabbingConstrained, {
-        '@contextualsCount': Drupal.formatPlural(contextuals.length, this.strings.contextualsCount.singular, this.strings.contextualsCount.plural)
-      }));
-      Drupal.announce(this.strings.pressEsc);
+  EditToggleModel: Backbone.Model.extend({
+    defaults: {
+      // Indicates whether the toggle is currently in "view" or "edit" mode.
+      isViewing: true,
+      // Indicates whether the toggle should be visible or hidden.
+      isVisible: false,
+      // The set of elements that can be reached via the tab key when edit mode
+      // is enabled.
+      tabbingContext: null,
+      // The set of contextual links stored as an Array.
+      contextuals: []
     }
-    else {
-      Drupal.announce(this.strings.tabbingReleased)
-    }
-  },
-
-  /**
-   * Responds to the edit mode toggle toolbar button; Toggles edit mode.
-   *
-   * @param jQuery.Event event
-   */
-  onClick: function (event) {
-    this.model.set('isViewing', !this.model.get('isViewing'));
-    this.announceTabbingConstraint();
-    this.announcedOnce = true;
-    event.preventDefault();
-    event.stopPropagation();
-  },
+  }),
 
   /**
-   * Responds to esc and tab key press events.
-   *
-   * @param jQuery.Event event
+   * Handles edit mode toggle interactions.
    */
-  onKeypress: function (event) {
-    // Respond to tab key press; Call render so the state announcement is read.
-    // The first tab key press is tracked so that an annoucement about tabbing
-    // constraints can be raised if edit mode is enabled when this page loads.
-    if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) {
+  EditToggleView: Backbone.View.extend({
+
+    events: { 'click': 'onClick' },
+
+    // Tracks whether the tabbing constraint announcement has been read once yet.
+    announcedOnce: false,
+
+    /**
+     * Implements Backbone Views' initialize().
+     */
+    initialize: function () {
+
+      this.strings = this.options.strings;
+
+      this.model.on('change', this.render, this);
+      this.model.on('change:isViewing', this.persist, this);
+      this.model.on('change:isViewing', this.manageTabbing, this);
+
+      $(document).on('keyup', $.proxy(this.onKeypress, this));
+    },
+
+    /**
+     * Implements Backbone Views' render().
+     */
+    render: function () {
+      // Render the visibility.
+      this.$el.toggleClass('element-hidden', !this.model.get('isVisible'));
+
+      // Render the state.
+      var isViewing = this.model.get('isViewing');
+      this.$el.find('button')
+        .toggleClass('active', !isViewing)
+        .attr('aria-pressed', !isViewing);
+
+      return this;
+    },
+
+    /**
+     * Limits tabbing to the contextual links and edit mode toolbar tab.
+     *
+     * @param Drupal.contextualToolbar.models.EditToggleModel model
+     *   An EditToggleModel Backbone model.
+     * @param bool isViewing
+     *   The value of the isViewing attribute in the model.
+     */
+    manageTabbing: function (model, isViewing) {
+      var tabbingContext = this.model.get('tabbingContext');
+      // Always release an existing tabbing context.
+      if (tabbingContext) {
+        tabbingContext.release();
+      }
+      // Create a new tabbing context when edit mode is enabled.
+      if (!isViewing) {
+        tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual'));
+        this.model.set('tabbingContext', tabbingContext);
+      }
+    },
+
+    /**
+     * Model change handler; persists the isViewing value to localStorage.
+     *
+     * isViewing === true is the default, so only stores in localStorage when
+     * it's not the default value (i.e. false).
+     *
+     * @param Drupal.contextualToolbar.models.EditToggleModel model
+     *   An EditToggleModel Backbone model.
+     * @param bool isViewing
+     *   The value of the isViewing attribute in the model.
+     */
+    persist: function (model, isViewing) {
+      if (!isViewing) {
+        localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false');
+      }
+      else {
+        localStorage.removeItem('Drupal.contextualToolbar.isViewing');
+      }
+    },
+
+    /**
+     * Passes state update messsages to Drupal.announce.
+     */
+    announceTabbingConstraint: function () {
+      var isViewing = this.model.get('isViewing');
+
+      if (!isViewing) {
+        var contextuals = this.model.get('contextuals');
+        Drupal.announce(Drupal.t(this.strings.tabbingConstrained, {
+          '@contextualsCount': Drupal.formatPlural(contextuals.length, this.strings.contextualsCount.singular, this.strings.contextualsCount.plural)
+        }));
+        Drupal.announce(this.strings.pressEsc);
+      }
+      else {
+        Drupal.announce(this.strings.tabbingReleased);
+      }
+    },
+
+    /**
+     * Responds to the edit mode toggle toolbar button; Toggles edit mode.
+     *
+     * @param jQuery.Event event
+     */
+    onClick: function (event) {
+      this.model.set('isViewing', !this.model.get('isViewing'));
       this.announceTabbingConstraint();
-      // Set announce to true so that this conditional block won't be run again.
       this.announcedOnce = true;
+      event.preventDefault();
+      event.stopPropagation();
+    },
+
+    /**
+     * Responds to esc and tab key press events.
+     *
+     * @param jQuery.Event event
+     */
+    onKeypress: function (event) {
+      // Respond to tab key press; Call render so the state announcement is read.
+      // The first tab key press is tracked so that an annoucement about tabbing
+      // constraints can be raised if edit mode is enabled when this page loads.
+      if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) {
+        this.announceTabbingConstraint();
+        // Set announce to true so that this conditional block won't be run again.
+        this.announcedOnce = true;
+      }
+      // Respond to the ESC key. Exit out of edit mode.
+      if (event.keyCode === 27) {
+        this.model.set('isViewing', true);
+      }
     }
-    // Respond to the ESC key. Exit out of edit mode.
-    if (event.keyCode === 27) {
-      this.model.set('isViewing', true);
-    }
-  }
-});
+  })
+};
+
+Drupal.contextualToolbar.collection = new Backbone.Collection([], { model: Drupal.contextualToolbar.EditToggleModel });
 
-})(jQuery, Backbone, Drupal, document, localStorage);
+})(jQuery, Drupal, Backbone);
