diff --git a/src/utils/bv-hover-swap.js b/src/utils/bv-hover-swap.js
new file mode 100644
index 00000000000..b605f17d541
--- /dev/null
+++ b/src/utils/bv-hover-swap.js
@@ -0,0 +1,66 @@
+import Vue from './vue'
+import { eventOn, eventOff } from './dom'
+
+// --- Constants ---
+
+const EVENT_OPTIONS = { passive: true }
+
+// @vue/component
+export const BVHoverSwap = /*#__PURE__*/ Vue.extend({
+ name: 'BVHoverSwap',
+ props: {
+ tag: {
+ type: String,
+ default: 'div'
+ },
+ parent: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ isHovered: false
+ }
+ },
+ watch: {
+ parent() {
+ this.listen(true)
+ }
+ },
+ created() {
+ // Create non-reactive property
+ this.$_hoverEl = null
+ },
+ mounted() {
+ this.$nextTick(() => this.listen(true))
+ },
+ updated() /* istanbul ignore next */ {
+ this.$nextTick(() => this.listen(true))
+ },
+ beforeDestroy() {
+ this.listen(false)
+ this.$_hoverEl = null
+ },
+ methods: {
+ listen(on) {
+ const el = this.parent ? this.$el.parentElement || this.$el : this.$el
+ if (on && this.$_hoverEl !== el) {
+ this.listen(false)
+ this.$_hoverEl = el
+ }
+ const method = on ? eventOn : eventOff
+ method(this.$_hoverEl, 'mouseenter', this.handleHover, EVENT_OPTIONS)
+ method(this.$_hoverEl, 'mouseleave', this.handleHover, EVENT_OPTIONS)
+ },
+ handleHover(evt) {
+ this.isHovered = evt.type === 'mouseenter'
+ }
+ },
+ render(h) {
+ const $scoped = this.$scopedSlots
+ const $default = $scoped.default || (() => h())
+ const $hovered = $scoped.hovered || $default
+ return h(this.tag, [this.isHovered ? $hovered() : $default()])
+ }
+})
diff --git a/src/utils/bv-hover-swap.spec.js b/src/utils/bv-hover-swap.spec.js
new file mode 100644
index 00000000000..484c2554637
--- /dev/null
+++ b/src/utils/bv-hover-swap.spec.js
@@ -0,0 +1,99 @@
+import { mount } from '@vue/test-utils'
+import { waitNT } from '../../tests/utils'
+import { BVHoverSwap } from './bv-hover-swap'
+
+describe('utils/bv-hoverswap', () => {
+ it('works', async () => {
+ const wrapper = mount(BVHoverSwap, {
+ slots: {
+ default: 'FOO',
+ hovered: 'BAR'
+ }
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ await waitNT(wrapper.vm)
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.text()).toBe('FOO')
+
+ wrapper.trigger('mouseenter')
+ await waitNT(wrapper.vm)
+
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.text()).toBe('BAR')
+
+ wrapper.trigger('mouseleave')
+ await waitNT(wrapper.vm)
+
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.text()).toBe('FOO')
+
+ wrapper.destroy()
+ })
+
+ it('works when `parent` is true ', async () => {
+ const app = {
+ props: {
+ parent: {
+ type: Boolean,
+ defaut: false
+ }
+ },
+ methods: {
+ foo() {
+ return this.$createElement('span', {}, 'FOO')
+ },
+ bar() {
+ return this.$createElement('span', {}, 'BAR')
+ }
+ },
+ render(h) {
+ const $content = h(BVHoverSwap, {
+ props: { parent: this.parent },
+ scopedSlots: { default: this.foo, hovered: this.bar }
+ })
+ return h('div', {}, [$content])
+ }
+ }
+ const wrapper = mount(app, {
+ propsData: {
+ parent: true
+ }
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ await waitNT(wrapper.vm)
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.find('div > div').exists()).toBe(true)
+ expect(wrapper.find('div > div').is(BVHoverSwap)).toBe(true)
+ expect(wrapper.text()).toBe('FOO')
+
+ wrapper.trigger('mouseenter')
+ await waitNT(wrapper.vm)
+
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.text()).toBe('BAR')
+
+ wrapper.trigger('mouseleave')
+ await waitNT(wrapper.vm)
+
+ expect(wrapper.text()).toBe('FOO')
+
+ wrapper.setProps({
+ parent: false
+ })
+ await waitNT(wrapper.vm)
+
+ wrapper.trigger('mouseenter')
+ await waitNT(wrapper.vm)
+
+ expect(wrapper.text()).toBe('FOO')
+
+ wrapper.find('div > div').trigger('mouseenter')
+ await waitNT(wrapper.vm)
+
+ expect(wrapper.text()).toBe('BAR')
+
+ wrapper.destroy()
+ })
+})
diff --git a/src/utils/bv-transition.js b/src/utils/bv-transition.js
index 320acb80b7e..b74e2d74f2a 100644
--- a/src/utils/bv-transition.js
+++ b/src/utils/bv-transition.js
@@ -24,6 +24,7 @@ const FADE_PROPS = {
leaveActiveClass: 'fade'
}
+// @vue/component
export const BVTransition = /*#__PURE__*/ Vue.extend({
name: 'BVTransition',
functional: true,