diff --git a/core/misc/offset/fragment.js b/core/misc/offset/fragment.js
new file mode 100644
index 0000000..ac40fb0
--- /dev/null
+++ b/core/misc/offset/fragment.js
@@ -0,0 +1,26 @@
+/**
+ * @file
+ * Offsets the window to allow fragments navigation with fixed positioning elements.
+ */
+(function ($, offset) {
+
+  "use strict";
+
+  function adjustOffset() {
+    var hash = window.location.hash;
+    if (hash) {
+      var element = document.getElementById(hash.replace('#', ''));
+      if (element) {
+        // Display the element in the viewport.
+        element.scrollIntoView();
+        // Scroll up by the amount of the top offset.
+        setTimeout(function () { window.scrollBy(0, -offset.top(true)); }, 5);
+      }
+    }
+  }
+
+  // Checks for a fragment on page load, outside Drupal behaviors.
+  $(document).ready(adjustOffset);
+  $(window).on('hashchange', adjustOffset);
+
+})(jQuery, Drupal.offset);
diff --git a/core/misc/offset/html5forms.js b/core/misc/offset/html5forms.js
new file mode 100644
index 0000000..13425d3
--- /dev/null
+++ b/core/misc/offset/html5forms.js
@@ -0,0 +1,15 @@
+/**
+ * @file
+ * Focus the HTML5 validation error in view.
+ */
+(function (offset) {
+
+  "use strict";
+
+  document.addEventListener('invalid', function (event) {
+    console.log('adjust: ' + event.target.id + ' of ' + offset.top());
+  }, true);
+
+
+
+})(Drupal.offset);
diff --git a/core/misc/offset/offset.js b/core/misc/offset/offset.js
new file mode 100644
index 0000000..091e03d
--- /dev/null
+++ b/core/misc/offset/offset.js
@@ -0,0 +1,50 @@
+/**
+ * @file
+ * Common methods to get or set an offset
+ */
+(function ($, Drupal) {
+
+  "use strict";
+
+  Drupal.offset = {
+    /**
+     * Holds the cached values.
+     */
+    values: {},
+
+    /**
+     * Provide the cached or recomputed value for the offset.
+     *
+     * @param direction
+     * @param reset
+     *
+     * @return {Number}
+     */
+    getOffset: function (direction, reset) {
+      if (reset === true || isNaN(this.values[direction])) {
+        this.values[direction] = this.compute(document, direction);
+      }
+      var offset = this.values[direction];
+      return !isNaN(offset) ?  offset : 0;
+    },
+
+    top: function (reset) { return this.getOffset('top', reset); },
+    right: function (reset) { return this.getOffset('right', reset); },
+    bottom: function (reset) { return this.getOffset('bottom', reset); },
+    left: function (reset) { return this.getOffset('left', reset); },
+
+    /**
+     * Sum all [data-offset-top] values and cache it.
+     */
+    compute: function (scope, direction) {
+      var offsets = $(scope).find(':visible[data-offset-' + direction + ']');
+      var value, sum = 0;
+      for (var i = 0, il = offsets.length; i < il; i++) {
+        value = parseInt(offsets[i].getAttribute('data-offset-top'), 10);
+        sum += !isNaN(value) ? value : 0;
+      }
+      return sum;
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/core/misc/tableheader.js b/core/misc/tableheader.js
index 0d4f7cd..76ab5d0 100644
--- a/core/misc/tableheader.js
+++ b/core/misc/tableheader.js
@@ -1,4 +1,4 @@
-(function ($, Drupal) {
+(function ($, Drupal, offset) {
 
 "use strict";
 
@@ -7,7 +7,10 @@
  */
 Drupal.behaviors.tableHeader = {
   attach: function (context) {
-    $(window).one('scroll.TableHeaderInit', {context: context}, tableHeaderInitHandler);
+    var $tables = $(context).find('table.sticky-enabled').once('tableheader');
+    for (var i = 0, il = $tables.length; i < il; i++) {
+      TableHeader.tables.push(new TableHeader($tables[i]));
+    }
   }
 };
 
@@ -15,14 +18,6 @@ function scrollValue(position) {
   return document.documentElement[position] || document.body[position];
 }
 
-// Select and initilize sticky table headers.
-function tableHeaderInitHandler(e) {
-  var $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
-  for (var i = 0, il = $tables.length; i < il; i++) {
-    TableHeader.tables.push(new TableHeader($tables[i]));
-  }
-}
-
 // Helper method to loop through tables and execute a method.
 function forTables(method, arg) {
   var tables = TableHeader.tables;
@@ -41,8 +36,7 @@ function tableHeaderOnScrollHandler(e) {
 
 function tableHeaderOffsetChangeHandler(e) {
   // Compute the new offset value.
-  TableHeader.computeOffsetTop();
-  forTables('stickyPosition', TableHeader.offsetTop);
+  forTables('stickyPosition', offset.top(true));
 }
 
 // Bind event that need to change all tables.
@@ -119,28 +113,7 @@ $.extend(TableHeader, {
    *
    * @type {Array}
    */
-  tables: [],
-
-  /**
-   * Cache of computed offset value.
-   *
-   * @type {Number}
-   */
-  offsetTop: 0,
-
-  /**
-   * Sum all [data-offset-top] values and cache it.
-   */
-  computeOffsetTop: function () {
-    var $offsets = $('[data-offset-top]');
-    var value, sum = 0;
-    for (var i = 0, il = $offsets.length; i < il; i++) {
-      value = parseInt($offsets[i].getAttribute('data-offset-top'), 10);
-      sum += !isNaN(value) ? value : 0;
-    }
-    this.offsetTop = sum;
-    return sum;
-  }
+  tables: []
 });
 
 /**
@@ -211,7 +184,7 @@ $.extend(TableHeader.prototype, {
    */
   checkStickyVisible: function () {
     var scrollTop = scrollValue('scrollTop');
-    var tableTop = this.tableOffset.top - TableHeader.offsetTop;
+    var tableTop = this.tableOffset.top - offset.top();
     var tableBottom = tableTop + this.tableHeight;
     var visible = false;
 
@@ -244,14 +217,6 @@ $.extend(TableHeader.prototype, {
    *   Event being triggered.
    */
   recalculateSticky: function (event) {
-    // Update table size.
-    this.tableHeight = this.$originalTable[0].clientHeight;
-
-    // Update offset top.
-    TableHeader.computeOffsetTop();
-    this.tableOffset = this.$originalTable.offset();
-    this.stickyPosition(TableHeader.offsetTop);
-
     // Update columns width.
     var $that = null;
     var $stickyCell = null;
@@ -270,11 +235,21 @@ $.extend(TableHeader.prototype, {
         $stickyCell.css('display', 'none');
       }
     }
+
+    // Update table size.
+    this.tableHeight = this.$originalTable[0].clientHeight;
+
+    // Update offset top.
+    this.tableOffset = this.$originalTable.offset();
+    var height = this.$stickyTable.height();
+    // Since the tableheader offset is counted in the offset, substract it.
     this.$stickyTable.css('width', this.$originalTable.outerWidth());
+    this.$stickyTable.attr('data-offset-top', height);
+    this.stickyPosition(offset.top() - height);
   }
 });
 
 // Expose constructor in the public space.
 Drupal.TableHeader = TableHeader;
 
-}(jQuery, Drupal));
+}(jQuery, Drupal, Drupal.offset));
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index cdf7b90..9eb30bf 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1218,6 +1218,7 @@ function system_library_info() {
     ),
     'dependencies' => array(
       array('system', 'jquery'),
+      array('system', 'drupal.offset'),
     ),
   );
 
@@ -1246,6 +1247,20 @@ function system_library_info() {
     ),
   );
 
+  // Drupal's offset utilites.
+  $libraries['drupal.offset'] = array(
+    'title' => 'Drupal offset',
+    'version' => VERSION,
+    'js' => array(
+      'core/misc/offset/offset.js' => array('group' => JS_LIBRARY),
+      'core/misc/offset/fragment.js' => array('group' => JS_LIBRARY),
+      'core/misc/offset/html5forms.js' => array('group' => JS_LIBRARY),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+    ),
+  );
+
   // Drupal's batch API.
   $libraries['drupal.batch'] = array(
     'title' => 'Drupal batch API',
