diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js
index d132d7d245dde5319d5d690aff95d5d8140af223..a3e88ad6eb661d9a62fbca60c1bf86631def7988 100644
--- a/js/src/dom/selectorEngine.js
+++ b/js/src/dom/selectorEngine.js
@@ -1,3 +1,5 @@
+import Util from '../util'
+
 /**
  * --------------------------------------------------------------------------
  * Bootstrap (v4.0.0-beta): dom/selectorEngine.js
@@ -27,17 +29,13 @@ const SelectorEngine = (() => {
   if (!Element.prototype.closest) {
     fnClosest = (element, selector) => {
       let ancestor = element
-      if (!document.documentElement.contains(element)) {
-        return null
-      }
-
       do {
         if (fnMatches.call(ancestor, selector)) {
           return ancestor
         }
 
         ancestor = ancestor.parentElement
-      } while (ancestor !== null)
+      } while (ancestor !== null && ancestor.nodeType === Node.ELEMENT_NODE)
 
       return null
     }
@@ -48,12 +46,67 @@ const SelectorEngine = (() => {
     }
   }
 
+  const scopeSelectorRegex = /:scope\b/
+  const supportScopeQuery = (() => {
+    const element = document.createElement('div')
+    try {
+      element.querySelectorAll(':scope *')
+    } catch (e) {
+      return false
+    }
+
+    return true
+  })()
+
+  let findFn = null
+  let findOneFn = null
+  if (supportScopeQuery) {
+    findFn = Element.prototype.querySelectorAll
+    findOneFn = Element.prototype.querySelector
+  } else {
+    findFn = function (selector) {
+      if (!scopeSelectorRegex.test(selector)) {
+        return this.querySelectorAll(selector)
+      }
+
+      const hasId = Boolean(this.id)
+      if (!hasId) {
+        this.id = Util.getUID('scope')
+      }
+
+      let nodeList = null
+      try {
+        selector = selector.replace(scopeSelectorRegex, `#${this.id}`)
+        nodeList = this.querySelectorAll(selector)
+      } finally {
+        if (!hasId) {
+          this.removeAttribute('id')
+        }
+      }
+
+      return nodeList
+    }
+
+    findOneFn = function (selector) {
+      if (!scopeSelectorRegex.test(selector)) {
+        return this.querySelector(selector)
+      }
+
+      const matches = findFn.call(this, selector)
+      if (typeof matches[0] !== 'undefined') {
+        return matches[0]
+      }
+
+      return null
+    }
+  }
+
   return {
     matches(element, selector) {
       return fnMatches.call(element, selector)
     },
 
-    find(selector, element = document) {
+    find(selector, element = document.documentElement) {
       if (typeof selector !== 'string') {
         return null
       }
@@ -62,21 +115,24 @@ const SelectorEngine = (() => {
         return SelectorEngine.findOne(selector, element)
       }
 
-      return element.querySelectorAll(selector)
+      return findFn.call(element, selector)
     },
 
-    findOne(selector, element = document) {
+    findOne(selector, element = document.documentElement) {
       if (typeof selector !== 'string') {
         return null
       }
 
-      let selectorType = 'querySelector'
-      if (selector.indexOf('#') === 0) {
-        selectorType = 'getElementById'
-        selector = selector.substr(1, selector.length)
+      return findOneFn.call(element, selector)
+    },
+
+    children(element, selector) {
+      if (typeof selector !== 'string') {
+        return null
       }
 
-      return element[selectorType](selector)
+      const children = Util.makeArray(element.children)
+      return children.filter((child) => this.matches(child, selector))
     },
 
     closest(element, selector) {
diff --git a/js/src/tab.js b/js/src/tab.js
index cb80997afc8bf106ee7a8d9a1448dd27a317afe9..934341348039501d9031cd153ccd79c1772e70f9 100644
--- a/js/src/tab.js
+++ b/js/src/tab.js
@@ -5,7 +5,9 @@
  * --------------------------------------------------------------------------
  */
 
-import $ from 'jquery'
+import Data from './dom/data'
+import EventHandler from './dom/eventHandler'
+import SelectorEngine from './dom/selectorEngine'
 import Util from './util'
 
 /**
@@ -19,7 +21,6 @@ const VERSION            = '4.3.1'
 const DATA_KEY           = 'bs.tab'
 const EVENT_KEY          = `.${DATA_KEY}`
 const DATA_API_KEY       = '.data-api'
-const JQUERY_NO_CONFLICT = $.fn[NAME]
 
 const Event = {
   HIDE           : `hide${EVENT_KEY}`,
@@ -41,10 +42,10 @@ const Selector = {
   DROPDOWN              : '.dropdown',
   NAV_LIST_GROUP        : '.nav, .list-group',
   ACTIVE                : '.active',
-  ACTIVE_UL             : '> li > .active',
+  ACTIVE_UL             : ':scope > li > .active',
   DATA_TOGGLE           : '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',
   DROPDOWN_TOGGLE       : '.dropdown-toggle',
-  DROPDOWN_ACTIVE_CHILD : '> .dropdown-menu .active'
+  DROPDOWN_ACTIVE_CHILD : ':scope > .dropdown-menu .active'
 }
 
 /**
@@ -56,6 +57,8 @@ const Selector = {
 class Tab {
   constructor(element) {
     this._element = element
+
+    Data.setData(this._element, DATA_KEY, this)
   }
 
   // Getters
@@ -68,39 +71,37 @@ class Tab {
 
   show() {
     if (this._element.parentNode &&
-        this._element.parentNode.nodeType === Node.ELEMENT_NODE &&
-        $(this._element).hasClass(ClassName.ACTIVE) ||
-        $(this._element).hasClass(ClassName.DISABLED)) {
+      this._element.parentNode.nodeType === Node.ELEMENT_NODE &&
+      this._element.classList.contains(ClassName.ACTIVE) ||
+      this._element.classList.contains(ClassName.DISABLED)) {
       return
     }
 
     let target
     let previous
-    const listElement = $(this._element).closest(Selector.NAV_LIST_GROUP)[0]
+    const listElement = SelectorEngine.closest(this._element, Selector.NAV_LIST_GROUP)
     const selector = Util.getSelectorFromElement(this._element)
 
     if (listElement) {
       const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector.ACTIVE_UL : Selector.ACTIVE
-      previous = $.makeArray($(listElement).find(itemSelector))
+      previous = Util.makeArray(SelectorEngine.find(itemSelector, listElement))
       previous = previous[previous.length - 1]
     }
 
-    const hideEvent = $.Event(Event.HIDE, {
-      relatedTarget: this._element
-    })
-
-    const showEvent = $.Event(Event.SHOW, {
-      relatedTarget: previous
-    })
+    let hideEvent = null
 
     if (previous) {
-      $(previous).trigger(hideEvent)
+      hideEvent = EventHandler.trigger(previous, Event.HIDE, {
+        relatedTarget: this._element
+      })
     }
 
-    $(this._element).trigger(showEvent)
+    const showEvent = EventHandler.trigger(this._element, Event.SHOW, {
+      relatedTarget: previous
+    })
 
-    if (showEvent.isDefaultPrevented() ||
-        hideEvent.isDefaultPrevented()) {
+    if (showEvent.defaultPrevented ||
+      hideEvent !== null && hideEvent.defaultPrevented) {
       return
     }
 
@@ -114,16 +115,12 @@ class Tab {
     )
 
     const complete = () => {
-      const hiddenEvent = $.Event(Event.HIDDEN, {
+      EventHandler.trigger(previous, Event.HIDDEN, {
         relatedTarget: this._element
       })
-
-      const shownEvent = $.Event(Event.SHOWN, {
+      EventHandler.trigger(this._element, Event.SHOWN, {
         relatedTarget: previous
       })
-
-      $(previous).trigger(hiddenEvent)
-      $(this._element).trigger(shownEvent)
     }
 
     if (target) {
@@ -134,7 +131,7 @@ class Tab {
   }
 
   dispose() {
-    $.removeData(this._element, DATA_KEY)
+    Data.removeData(this._element, DATA_KEY)
     this._element = null
   }
 
@@ -142,11 +139,13 @@ class Tab {
 
   _activate(element, container, callback) {
     const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL')
-      ? $(container).find(Selector.ACTIVE_UL)
-      : $(container).children(Selector.ACTIVE)
+      ? SelectorEngine.find(Selector.ACTIVE_UL, container)
+      : SelectorEngine.children(container, Selector.ACTIVE)
+
+    const active          = activeElements[0]
+    const isTransitioning = callback &&
+      (active && active.classList.contains(ClassName.FADE))
 
-    const active = activeElements[0]
-    const isTransitioning = callback && (active && $(active).hasClass(ClassName.FADE))
     const complete = () => this._transitionComplete(
       element,
       active,
@@ -155,10 +154,9 @@ class Tab {
 
     if (active && isTransitioning) {
       const transitionDuration = Util.getTransitionDurationFromElement(active)
+      active.classList.remove(ClassName.SHOW)
 
-      $(active)
-        .removeClass(ClassName.SHOW)
-        .one(Util.TRANSITION_END, complete)
+      EventHandler.one(active, Util.TRANSITION_END, complete)
       Util.emulateTransitionEnd(active, transitionDuration)
     } else {
       complete()
@@ -167,14 +165,12 @@ class Tab {
 
   _transitionComplete(element, active, callback) {
     if (active) {
-      $(active).removeClass(ClassName.ACTIVE)
+      active.classList.remove(ClassName.ACTIVE)
 
-      const dropdownChild = $(active.parentNode).find(
-        Selector.DROPDOWN_ACTIVE_CHILD
-      )[0]
+      const dropdownChild = SelectorEngine.findOne(Selector.DROPDOWN_ACTIVE_CHILD, active.parentNode)
 
       if (dropdownChild) {
-        $(dropdownChild).removeClass(ClassName.ACTIVE)
+        dropdownChild.classList.remove(ClassName.ACTIVE)
       }
 
       if (active.getAttribute('role') === 'tab') {
@@ -182,7 +178,7 @@ class Tab {
       }
     }
 
-    $(element).addClass(ClassName.ACTIVE)
+    element.classList.add(ClassName.ACTIVE)
     if (element.getAttribute('role') === 'tab') {
       element.setAttribute('aria-selected', true)
     }
@@ -193,13 +189,12 @@ class Tab {
       element.classList.add(ClassName.SHOW)
     }
 
-    if (element.parentNode && $(element.parentNode).hasClass(ClassName.DROPDOWN_MENU)) {
-      const dropdownElement = $(element).closest(Selector.DROPDOWN)[0]
+    if (element.parentNode && element.parentNode.classList.contains(ClassName.DROPDOWN_MENU)) {
+      const dropdownElement = SelectorEngine.closest(element, Selector.DROPDOWN)
 
       if (dropdownElement) {
-        const dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(Selector.DROPDOWN_TOGGLE))
-
-        $(dropdownToggleList).addClass(ClassName.ACTIVE)
+        Util.makeArray(dropdownElement.querySelectorAll(Selector.DROPDOWN_TOGGLE))
+          .forEach((dropdown) => dropdown.classList.add(ClassName.ACTIVE))
       }
 
       element.setAttribute('aria-expanded', true)
@@ -214,13 +209,7 @@ class Tab {
 
   static _jQueryInterface(config) {
     return this.each(function () {
-      const $this = $(this)
-      let data = $this.data(DATA_KEY)
-
-      if (!data) {
-        data = new Tab(this)
-        $this.data(DATA_KEY, data)
-      }
+      const data = Data.getData(this, DATA_KEY) || new Tab(this)
 
       if (typeof config === 'string') {
         if (typeof data[config] === 'undefined') {
@@ -238,11 +227,12 @@ class Tab {
  * ------------------------------------------------------------------------
  */
 
-$(document)
-  .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
-    event.preventDefault()
-    Tab._jQueryInterface.call($(this), 'show')
-  })
+EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
+  event.preventDefault()
+
+  const data = Data.getData(this, DATA_KEY) || new Tab(this)
+  data.show()
+})
 
 /**
  * ------------------------------------------------------------------------
@@ -250,11 +240,15 @@ $(document)
  * ------------------------------------------------------------------------
  */
 
-$.fn[NAME] = Tab._jQueryInterface
-$.fn[NAME].Constructor = Tab
-$.fn[NAME].noConflict = () => {
-  $.fn[NAME] = JQUERY_NO_CONFLICT
-  return Tab._jQueryInterface
+const $ = Util.jQuery
+if (typeof $ !== 'undefined') {
+  const JQUERY_NO_CONFLICT = $.fn[NAME]
+  $.fn[NAME]               = Tab._jQueryInterface
+  $.fn[NAME].Constructor   = Tab
+  $.fn[NAME].noConflict    = () => {
+    $.fn[NAME] = JQUERY_NO_CONFLICT
+    return Tab._jQueryInterface
+  }
 }
 
 export default Tab
diff --git a/js/tests/unit/tab.js b/js/tests/unit/tab.js
index 4491e14948c4fe02dd161bcf549720dd734c4102..00dcac18f64f436d1254d61ec055078c6b4484b0 100644
--- a/js/tests/unit/tab.js
+++ b/js/tests/unit/tab.js
@@ -320,7 +320,7 @@ $(function () {
         '</ul>'
     var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
 
-    $tabs.find('li:last-child a').trigger('click')
+    EventHandler.trigger($tabs.find('li:last-child a')[0], 'click')
     assert.notOk($tabs.find('li:first-child a').hasClass('active'))
     assert.ok($tabs.find('li:last-child a').hasClass('active'))
   })
@@ -339,7 +339,7 @@ $(function () {
         '</ul>'
     var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
 
-    $tabs.find('li:first-child a').trigger('click')
+    EventHandler.trigger($tabs.find('li:first-child a')[0], 'click')
     assert.ok($tabs.find('li:first-child a').hasClass('active'))
     assert.notOk($tabs.find('li:last-child a').hasClass('active'))
     assert.notOk($tabs.find('li:last-child .dropdown-menu a:first-child').hasClass('active'))
@@ -378,9 +378,10 @@ $(function () {
 
     $('#tab1').on('shown.bs.tab', function () {
       assert.ok($('#x-tab1').hasClass('active'))
-      $('#tabNested2').trigger($.Event('click'))
+      EventHandler.trigger($('#tabNested2')[0], 'click')
     })
-      .trigger($.Event('click'))
+
+    EventHandler.trigger($('#tab1')[0], 'click')
   })
 
   QUnit.test('should not remove fade class if no active pane is present', function (assert) {
@@ -410,9 +411,11 @@ $(function () {
 
             done()
           })
-          .trigger($.Event('click'))
+
+        EventHandler.trigger($('#tab-home')[0], 'click')
       })
-      .trigger($.Event('click'))
+
+    EventHandler.trigger($('#tab-profile')[0], 'click')
   })
 
   QUnit.test('should handle removed tabs', function (assert) {
diff --git a/js/tests/visual/tab.html b/js/tests/visual/tab.html
index 3b8ce4026f49b9f983cd905720966657d1ad8be8..78e573403918bacc29c04f50f4e738b65c4f12c2 100644
--- a/js/tests/visual/tab.html
+++ b/js/tests/visual/tab.html
@@ -227,7 +227,10 @@
 
     <script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script>
     <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
+    <script src="../../dist/dom/data.js"></script>
     <script src="../../dist/dom/eventHandler.js"></script>
+    <script src="../../dist/dom/manipulator.js"></script>
+    <script src="../../dist/dom/selectorEngine.js"></script>
     <script src="../../dist/util.js"></script>
     <script src="../../dist/tab.js"></script>
     <script src="../../dist/dropdown.js"></script>