diff --git a/js/src/carousel.js b/js/src/carousel.js
index 15a56bd76a63d2c6e5de476b06acafad2d793cc3..352b238490eab8a0a0ead11fe700ea5aaee8ddf4 100644
--- a/js/src/carousel.js
+++ b/js/src/carousel.js
@@ -562,8 +562,8 @@ class Carousel {
     }
 
     const config = {
-      ...Util.getDataAttributes(target),
-      ...Util.getDataAttributes(this)
+      ...Manipulator.getDataAttributes(target),
+      ...Manipulator.getDataAttributes(this)
     }
     const slideIndex = this.getAttribute('data-slide-to')
 
diff --git a/js/src/collapse.js b/js/src/collapse.js
index d04743d03953c2f2f01b34c438a941fe0249ddf8..eebac13bbf36e809471b1457e8617ed8448e1fff 100644
--- a/js/src/collapse.js
+++ b/js/src/collapse.js
@@ -7,6 +7,7 @@
 
 import Data from './dom/data'
 import EventHandler from './dom/eventHandler'
+import Manipulator from './dom/manipulator'
 import SelectorEngine from './dom/selectorEngine'
 import Util from './util'
 
@@ -347,7 +348,7 @@ class Collapse {
     let data      = Data.getData(element, DATA_KEY)
     const _config = {
       ...Default,
-      ...Util.getDataAttributes(element),
+      ...Manipulator.getDataAttributes(element),
       ...typeof config === 'object' && config ? config : {}
     }
 
@@ -391,7 +392,7 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (
     event.preventDefault()
   }
 
-  const triggerData      = Util.getDataAttributes(this)
+  const triggerData      = Manipulator.getDataAttributes(this)
   const selector         = Util.getSelectorFromElement(this)
   const selectorElements = Util.makeArray(SelectorEngine.find(selector))
 
diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js
index 201902b77e5671be23a1441dd7715a219ee947c6..db3113f88d6e6706da4fc02726f40f1b379a174d 100644
--- a/js/src/dom/manipulator.js
+++ b/js/src/dom/manipulator.js
@@ -1,12 +1,32 @@
-import Util from '../util'
-
 /**
  * --------------------------------------------------------------------------
- * Bootstrap (v4.0.0-beta): dom/manipulator.js
+ * Bootstrap (v4.1.1): dom/manipulator.js
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * --------------------------------------------------------------------------
  */
 
+const regexDataKey = /[A-Z]/g
+
+function normalizeData(val) {
+  if (val === 'true') {
+    return true
+  } else if (val === 'false') {
+    return false
+  } else if (val === 'null') {
+    return null
+  } else if (val === Number(val).toString()) {
+    return Number(val)
+  } else if (val === '') {
+    return null
+  }
+
+  return val
+}
+
+function normalizeDataKey(key) {
+  return key.replace(regexDataKey, (chr) => chr.toLowerCase())
+}
+
 const Manipulator = {
   setChecked(input, val) {
     if (input instanceof HTMLInputElement) {
@@ -23,21 +43,55 @@ const Manipulator = {
   },
 
   setDataAttribute(element, key, value) {
-    const $ = Util.jQuery
-    if (typeof $ !== 'undefined') {
-      $(element).data(key, value)
-    }
-
-    element.setAttribute(`data-${key.replace(/[A-Z]/g, (chr) => `-${chr.toLowerCase()}`)}`, value)
+    element.setAttribute(`data-${normalizeDataKey(key)}`, value)
   },
 
   removeDataAttribute(element, key) {
-    const $ = Util.jQuery
-    if (typeof $ !== 'undefined') {
-      $(element).removeData(key)
+    element.removeAttribute(`data-${normalizeDataKey(key)}`)
+  },
+
+  getDataAttributes(element) {
+    if (typeof element === 'undefined' || element === null) {
+      return {}
+    }
+
+    let attributes
+    if (Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'dataset')) {
+      attributes = {
+        ...element.dataset
+      }
+    } else {
+      attributes = {}
+      for (let i = 0; i < element.attributes.length; i++) {
+        const attribute = element.attributes[i]
+
+        if (attribute.nodeName.indexOf('data-') !== -1) {
+          // remove 'data-' part of the attribute name
+          const attributeName = attribute
+            .nodeName
+            .substring('data-'.length)
+            .replace(/-./g, (str) => str.charAt(1).toUpperCase())
+
+          attributes[attributeName] = attribute.nodeValue
+        }
+      }
     }
 
-    element.removeAttribute(`data-${key.replace(/[A-Z]/g, (chr) => `-${chr.toLowerCase()}`)}`)
+    for (const key in attributes) {
+      if (!Object.prototype.hasOwnProperty.call(attributes, key)) {
+        continue
+      }
+
+      attributes[key] = normalizeData(attributes[key])
+    }
+
+    return attributes
+  },
+
+  getDataAttribute(element, key) {
+    return normalizeData(element
+      .getAttribute(`data-${normalizeDataKey(key)}`)
+    )
   },
 
   offset(element) {
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index 282e7645ffbe7b67fe44254067870774a5ba7ce5..b1487b64ad1b2dbd93c34940a7e58af5c0859118 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -267,7 +267,7 @@ class Dropdown {
   _getConfig(config) {
     config = {
       ...this.constructor.Default,
-      ...Util.getDataAttributes(this._element),
+      ...Manipulator.getDataAttributes(this._element),
       ...config
     }
 
diff --git a/js/src/modal.js b/js/src/modal.js
index 4f23fff741f5adbd5fa76f6cb9086022e5634dc7..0ecd6948f21b759175281b87f1dd6f9b7f9c5834 100644
--- a/js/src/modal.js
+++ b/js/src/modal.js
@@ -473,7 +473,7 @@ class Modal {
     // Restore fixed content padding
     Util.makeArray(SelectorEngine.find(Selector.FIXED_CONTENT))
       .forEach((element) => {
-        const padding = Util.getDataAttribute(element, 'padding-right')
+        const padding = Manipulator.getDataAttribute(element, 'padding-right')
         if (typeof padding !== 'undefined') {
           Manipulator.removeDataAttribute(element, 'padding-right')
           element.style.paddingRight = padding
@@ -483,7 +483,7 @@ class Modal {
     // Restore sticky content and navbar-toggler margin
     Util.makeArray(SelectorEngine.find(`${Selector.STICKY_CONTENT}`))
       .forEach((element) => {
-        const margin = Util.getDataAttribute(element, 'margin-right')
+        const margin = Manipulator.getDataAttribute(element, 'margin-right')
         if (typeof margin !== 'undefined') {
           Manipulator.removeDataAttribute(element, 'margin-right')
           element.style.marginRight = margin
@@ -491,17 +491,13 @@ class Modal {
       })
 
     // Restore body padding
-    const padding = Util.getDataAttribute(document.body, 'padding-right')
+    const padding = Manipulator.getDataAttribute(document.body, 'padding-right')
     if (typeof padding !== 'undefined') {
       Manipulator.removeDataAttribute(document.body, 'padding-right')
       document.body.style.paddingRight = padding
     } else {
       document.body.style.paddingRight = ''
     }
-
-    static _getInstance(element) {
-      return Data.getData(element, DATA_KEY)
-    }
   }
 
   _getScrollbarWidth() { // thx d.walsh
@@ -520,7 +516,7 @@ class Modal {
       let data = Data.getData(this, DATA_KEY)
       const _config = {
         ...Default,
-        ...Util.getDataAttributes(this),
+        ...Manipulator.getDataAttributes(this),
         ...typeof config === 'object' && config ? config : {}
       }
 
@@ -539,6 +535,10 @@ class Modal {
       }
     })
   }
+
+  static _getInstance(element) {
+    return Data.getData(element, DATA_KEY)
+  }
 }
 
 /**
@@ -557,8 +557,8 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (
 
   const config = Data.getData(target, DATA_KEY)
     ? 'toggle' : {
-      ...Util.getDataAttributes(target),
-      ...Util.getDataAttributes(this)
+      ...Manipulator.getDataAttributes(target),
+      ...Manipulator.getDataAttributes(this)
     }
 
   if (this.tagName === 'A' || this.tagName === 'AREA') {
diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js
index 458f5170e91cdda63aa92acb3b6da40f705ff254..f317284c9c5c23ffd6444ddd7e8d64297a1cf251 100644
--- a/js/src/scrollspy.js
+++ b/js/src/scrollspy.js
@@ -322,7 +322,7 @@ class ScrollSpy {
 
 EventHandler.on(window, Event.LOAD_DATA_API, () => {
   Util.makeArray(SelectorEngine.find(Selector.DATA_SPY))
-    .forEach((spy) => new ScrollSpy(spy, Util.getDataAttributes(spy)))
+    .forEach((spy) => new ScrollSpy(spy, Manipulator.getDataAttributes(spy)))
 })
 
 /**
diff --git a/js/src/tooltip.js b/js/src/tooltip.js
index 9b8b8263a9a3942d0176a74e90bee14ada672f3a..fbe9ed856a5f6d349f4395c6c1e72134a2b488c7 100644
--- a/js/src/tooltip.js
+++ b/js/src/tooltip.js
@@ -11,6 +11,7 @@ import {
 } from './tools/sanitizer'
 import Data from './dom/data'
 import EventHandler from './dom/eventHandler'
+import Manipulator from './dom/manipulator'
 import Popper from 'popper.js'
 import SelectorEngine from './dom/selectorEngine'
 import Util from './util'
@@ -671,7 +672,7 @@ class Tooltip {
   }
 
   _getConfig(config) {
-    const dataAttributes = Util.getDataAttributes(this.element)
+    const dataAttributes = Manipulator.getDataAttributes(this.element)
 
     Object.keys(dataAttributes)
       .forEach((dataAttr) => {
@@ -741,10 +742,6 @@ class Tooltip {
         .map((token) => token.trim())
         .forEach((tClass) => tip.classList.remove(tClass))
     }
-
-    static _getInstance(element) {
-      return Data.getData(element, DATA_KEY)
-    }
   }
 
   _handlePopperPlacementChange(popperData) {
@@ -793,6 +790,10 @@ class Tooltip {
       }
     })
   }
+
+  static _getInstance(element) {
+    return Data.getData(element, DATA_KEY)
+  }
 }
 
 /**
diff --git a/js/src/util.js b/js/src/util.js
index 78f5fe3fbe2ec0c997b948c787a2290e999b2263..0b7f492feea4e2630e9881cdb1d033aa9fd15513 100644
--- a/js/src/util.js
+++ b/js/src/util.js
@@ -22,20 +22,6 @@ function toType(obj) {
   return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase()
 }
 
-function normalizeData(val) {
-  if (val === 'true') {
-    return true
-  } else if (val === 'false') {
-    return false
-  } else if (val === 'null') {
-    return null
-  } else if (val === Number(val).toString()) {
-    return Number(val)
-  }
-
-  return val
-}
-
 const Util = {
 
   TRANSITION_END: 'bsTransitionEnd',
@@ -164,41 +150,6 @@ const Util = {
     return [nodeList]
   },
 
-  getDataAttributes(element) {
-    if (typeof element === 'undefined' || element === null) {
-      return {}
-    }
-
-    let attributes
-    if (Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'dataset')) {
-      attributes = this.extend({}, element.dataset)
-    } else {
-      attributes = {}
-      for (let i = 0; i < element.attributes.length; i++) {
-        const attribute = element.attributes[i]
-        if (attribute.nodeName.indexOf('data-') !== -1) {
-          // remove 'data-' part of the attribute name
-          const attributeName = attribute.nodeName.substring('data-'.length).replace(/-./g, (str) => str.charAt(1).toUpperCase())
-          attributes[attributeName] = attribute.nodeValue
-        }
-      }
-    }
-
-    for (const key in attributes) {
-      if (!Object.prototype.hasOwnProperty.call(attributes, key)) {
-        continue
-      }
-
-      attributes[key] = normalizeData(attributes[key])
-    }
-
-    return attributes
-  },
-
-  getDataAttribute(element, key) {
-    return normalizeData(element.getAttribute(`data-${key.replace(/[A-Z]/g, (chr) => `-${chr.toLowerCase()}`)}`))
-  },
-
   isVisible(element) {
     if (typeof element === 'undefined' || element === null) {
       return false
diff --git a/js/tests/index.html b/js/tests/index.html
index 19ff53ce8a38b297f47f04e7c4e1040ed22796a2..f4a99df44e5707c910c6573ef89612c9d78a3cf9 100644
--- a/js/tests/index.html
+++ b/js/tests/index.html
@@ -97,10 +97,11 @@
     </script>
 
     <!-- Transpiled Plugins -->
+    <script src="../dist/dom/polyfill.js"></script>
+    <script src="../dist/dom/manipulator.js"></script>
     <script src="../dist/dom/eventHandler.js"></script>
     <script src="../dist/dom/selectorEngine.js"></script>
     <script src="../dist/dom/data.js"></script>
-    <script src="../dist/dom/manipulator.js"></script>
     <script src="../dist/util.js"></script>
     <script src="../dist/alert.js"></script>
     <script src="../dist/button.js"></script>
@@ -116,6 +117,8 @@
 
     <!-- Unit Tests -->
     <script src="unit/dom/eventHandler.js"></script>
+    <script src="unit/dom/manipulator.js"></script>
+    <script src="unit/dom/data.js"></script>
     <script src="unit/alert.js"></script>
     <script src="unit/button.js"></script>
     <script src="unit/carousel.js"></script>
diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js
index 066165a14d63d9a4e995f0c1437f40b9e89eb019..469a95561f516305420a172987d9cdbb01b38861 100644
--- a/js/tests/karma.conf.js
+++ b/js/tests/karma.conf.js
@@ -140,6 +140,16 @@ if (bundle) {
         branches: 86,
         functions: 89,
         lines: 90
+      },
+      each: {
+        overrides: {
+          'js/src/dom/polyfill.js': {
+            statements: 39,
+            lines: 37,
+            branches: 19,
+            functions: 50
+          }
+        }
       }
     }
   }
diff --git a/js/tests/unit/dom/manipulator.js b/js/tests/unit/dom/manipulator.js
new file mode 100644
index 0000000000000000000000000000000000000000..19effa42316e35477e76c53e5c0706ed82f396b4
--- /dev/null
+++ b/js/tests/unit/dom/manipulator.js
@@ -0,0 +1,179 @@
+$(function () {
+  'use strict'
+
+  QUnit.module('manipulator')
+
+  QUnit.test('should be defined', function (assert) {
+    assert.expect(1)
+    assert.ok(Manipulator, 'Manipulator is defined')
+  })
+
+  QUnit.test('should set checked for input', function (assert) {
+    assert.expect(2)
+
+    var $input = $('<input type="checkbox" />').appendTo('#qunit-fixture')
+    Manipulator.setChecked($input[0], true)
+
+    assert.ok($input[0].checked)
+
+    Manipulator.setChecked($input[0], false)
+
+    assert.ok(!$input[0].checked)
+  })
+
+  QUnit.test('should not set checked for non input element', function (assert) {
+    assert.expect(1)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+    Manipulator.setChecked($div[0], true)
+
+    assert.ok(typeof $div[0].checked === 'undefined')
+  })
+
+  QUnit.test('should verify if an element is checked', function (assert) {
+    assert.expect(2)
+
+    var $input = $('<input type="checkbox" />').appendTo('#qunit-fixture')
+    Manipulator.setChecked($input[0], true)
+
+    assert.ok(Manipulator.isChecked($input[0]))
+
+    Manipulator.setChecked($input[0], false)
+
+    assert.ok(!Manipulator.isChecked($input[0]))
+  })
+
+  QUnit.test('should throw an error when the element is not an input', function (assert) {
+    assert.expect(1)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+    try {
+      Manipulator.isChecked($div[0])
+    } catch (e) {
+      assert.strictEqual(e.message, 'INPUT parameter is not an HTMLInputElement')
+    }
+  })
+
+  QUnit.test('should set data attribute', function (assert) {
+    assert.expect(1)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+
+    Manipulator.setDataAttribute($div[0], 'test', 'test')
+
+    assert.strictEqual($div[0].getAttribute('data-test'), 'test')
+  })
+
+  QUnit.test('should set data attribute in lower case', function (assert) {
+    assert.expect(1)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+
+    Manipulator.setDataAttribute($div[0], 'tEsT', 'test')
+
+    assert.strictEqual($div[0].getAttribute('data-test'), 'test')
+  })
+
+  QUnit.test('should get data attribute', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div data-test="null" />').appendTo('#qunit-fixture')
+
+    assert.strictEqual(Manipulator.getDataAttribute($div[0], 'test'), null)
+
+    var $div2 = $('<div data-test2="js" />').appendTo('#qunit-fixture')
+
+    assert.strictEqual(Manipulator.getDataAttribute($div2[0], 'tEst2'), 'js')
+  })
+
+  QUnit.test('should get data attributes', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div data-test="js" data-test2="js2" />').appendTo('#qunit-fixture')
+    var $div2 = $('<div data-test3="js" data-test4="js2" />').appendTo('#qunit-fixture')
+
+    assert.propEqual(Manipulator.getDataAttributes($div[0]), {
+      test: 'js',
+      test2: 'js2'
+    })
+
+    var stub = sinon
+      .stub(Object, 'getOwnPropertyDescriptor')
+      .callsFake(function () {
+        return false
+      })
+
+    assert.propEqual(Manipulator.getDataAttributes($div2[0]), {
+      test3: 'js',
+      test4: 'js2'
+    })
+
+    stub.restore()
+  })
+
+  QUnit.test('should remove data attribute', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+
+    Manipulator.setDataAttribute($div[0], 'test', 'test')
+
+    assert.strictEqual($div[0].getAttribute('data-test'), 'test')
+
+    Manipulator.removeDataAttribute($div[0], 'test')
+
+    assert.strictEqual($div[0].getAttribute('data-test'), null)
+  })
+
+  QUnit.test('should remove data attribute in lower case', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+
+    Manipulator.setDataAttribute($div[0], 'test', 'test')
+
+    assert.strictEqual($div[0].getAttribute('data-test'), 'test')
+
+    Manipulator.removeDataAttribute($div[0], 'tESt')
+
+    assert.strictEqual($div[0].getAttribute('data-test'), null)
+  })
+
+  QUnit.test('should return element offsets', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+
+    var offset = Manipulator.offset($div[0])
+
+    assert.ok(typeof offset.top === 'number')
+    assert.ok(typeof offset.left === 'number')
+  })
+
+  QUnit.test('should return element position', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div />').appendTo('#qunit-fixture')
+
+    var offset = Manipulator.position($div[0])
+
+    assert.ok(typeof offset.top === 'number')
+    assert.ok(typeof offset.left === 'number')
+  })
+
+  QUnit.test('should toggle class', function (assert) {
+    assert.expect(2)
+
+    var $div = $('<div class="test" />').appendTo('#qunit-fixture')
+
+    Manipulator.toggleClass($div[0], 'test')
+
+    assert.ok(!$div.hasClass('test'))
+
+    Manipulator.toggleClass($div[0], 'test')
+
+    assert.ok($div.hasClass('test'))
+
+    Manipulator.toggleClass(null)
+  })
+})
diff --git a/js/tests/unit/modal.js b/js/tests/unit/modal.js
index 7aa7a95b3cdcb79016d32d0c2ad65865c47f4955..0739f03782846857cb7aee435217ddbd9e70b6fa 100644
--- a/js/tests/unit/modal.js
+++ b/js/tests/unit/modal.js
@@ -422,7 +422,7 @@ $(function () {
 
     $('<div id="modal-test"/>')
       .on('hidden.bs.modal', function () {
-        assert.strictEqual(typeof $body.data('padding-right'), 'undefined', 'data-padding-right should be cleared after closing')
+        assert.strictEqual(document.body.getAttribute('data-padding-right'), null, 'data-padding-right should be cleared after closing')
         $body.removeAttr('style')
         done()
       })
@@ -488,7 +488,7 @@ $(function () {
 
     $('<div id="modal-test"/>')
       .on('hidden.bs.modal', function () {
-        assert.strictEqual(typeof $element.data('padding-right'), 'undefined', 'data-padding-right should be cleared after closing')
+        assert.strictEqual($element[0].getAttribute('data-padding-right'), null, 'data-padding-right should be cleared after closing')
         $element.remove()
         done()
       })
@@ -530,7 +530,7 @@ $(function () {
 
     $('<div id="modal-test"/>')
       .on('hidden.bs.modal', function () {
-        assert.strictEqual(typeof $element.data('margin-right'), 'undefined', 'data-margin-right should be cleared after closing')
+        assert.strictEqual($element[0].getAttribute('data-margin-right'), null, 'data-margin-right should be cleared after closing')
         $element.remove()
         done()
       })