vue-hover-effect

WebGL-powered image hover transition effects for Vue 3, built on top of Three.js and GSAP. Inspired by Robin Delaporte's hover-effect.
1,634
Install
npm install vue-hover-effect@0.0.8-stable.4
Latest Version:0.0.8-stable.4
Vue:3
License:MIT
Last Updated:Mar 11, 2026
Links: npm
Maintainer: sultandev

vue-hover-effect

WebGL-powered image hover transition effects for Vue 3, built on top of Three.js and GSAP. Inspired by Robin Delaporte's hover-effect.

Features

  • GPU-accelerated displacement map transitions between images
  • Two ready-to-use components: single two-image transition and multi-image carousel
  • Manual control via next / previous slot methods
  • Video texture support
  • Fully typed props — TypeScript autocompletion out of the box
  • SSR / Nuxt compatible — Three.js and GSAP are lazy-loaded client-side only

Installation

# npm
npm install vue-hover-effect

# pnpm
pnpm add vue-hover-effect

# yarn
yarn add vue-hover-effect

Peer dependencies

Three.js and GSAP are peer dependencies. Install them alongside the package:

npm install three gsap

Usage

SingleHoverEffect

Transitions between two images on hover (or manually via next / previous).

<script setup lang="ts">
import { SingleHoverEffect } from 'vue-hover-effect'
</script>

<template>
  <SingleHoverEffect
    image1="/images/photo-a.webp"
    image2="/images/photo-b.webp"
    displacement-image="/displacements/displacement.webp"
  />
</template>
With custom size

Use the distortion-class prop to apply your own CSS class to the canvas container:

<template>
  <SingleHoverEffect
    image1="/images/photo-a.webp"
    image2="/images/photo-b.webp"
    displacement-image="/displacements/displacement.webp"
    distortion-class="my-effect"
  />
</template>

<style>
.my-effect {
  width: 600px;
  height: 400px;
}
</style>
With manual controls

Use the controllers slot to wire up your own buttons. The slot exposes funcs.goNext() and funcs.goPrev():

<template>
  <SingleHoverEffect
    image1="/images/photo-a.webp"
    image2="/images/photo-b.webp"
    displacement-image="/displacements/displacement.webp"
    :hover="false"
    distortion-class="my-effect"
  >
    <template #controllers="{ funcs }">
      <button @click="funcs.goPrev()">Previous</button>
      <button @click="funcs.goNext()">Next</button>
    </template>
  </SingleHoverEffect>
</template>
With all options
<template>
  <SingleHoverEffect
    image1="/images/photo-a.webp"
    image2="/images/photo-b.webp"
    displacement-image="/displacements/displacement.webp"
    :images-ratio="1.5"
    :intensity="0.8"
    :speed-in="1.2"
    :speed-out="0.9"
    easing="power2.out"
    :hover="true"
  />
</template>
Props
Prop Type Required Default Description
image1 string Yes — URL of the first image
image2 string Yes — URL of the second image
displacementImage string No CDN map #1 URL of the displacement map texture
imagesRatio number No 1.0 Aspect ratio of the images (width / height)
intensity number No 1 Distortion intensity fallback for both images
intensity1 number No 1 Distortion intensity for the first image
intensity2 number No 1 Distortion intensity for the second image
angle number No Math.PI / 4 Displacement rotation angle fallback
angle1 number No — Rotation angle for the first image
angle2 number No — Rotation angle for the second image
speed number No — Transition speed fallback for both directions
speedIn number No 1.6 Duration (seconds) of the enter transition
speedOut number No 1.2 Duration (seconds) of the exit transition
hover boolean No true Trigger transitions automatically on hover
easing string No 'expo.out' GSAP easing string
video boolean No false Treat image1 / image2 as <video> sources
distortionClass string No '' CSS class applied to the canvas container element

MultipleHoverEffect

Cycles through an array of images with displacement transitions. Requires at least two images.

<script setup lang="ts">
import { MultipleHoverEffect } from 'vue-hover-effect'
</script>

<template>
  <MultipleHoverEffect
    :images="[
      '/images/photo-a.webp',
      '/images/photo-b.webp',
      '/images/photo-c.webp',
    ]"
    displacement-image="/displacements/displacement.webp"
  />
</template>
With manual controls
<template>
  <MultipleHoverEffect
    :images="[
      '/images/photo-a.webp',
      '/images/photo-b.webp',
      '/images/photo-c.webp',
    ]"
    displacement-image="/displacements/displacement.webp"
    :hover="false"
    distortion-class="my-gallery"
  >
    <template #controllers="{ funcs }">
      <button @click="funcs.goPrev()">Previous</button>
      <button @click="funcs.goNext()">Next</button>
    </template>
  </MultipleHoverEffect>
</template>
Props
Prop Type Required Default Description
images string[] Yes — Array of image URLs (minimum 2)
displacementImage string No CDN map #1 URL of the displacement map texture
imagesRatio number No 1.0 Aspect ratio of the images (width / height)
intensity1 number No 1 Distortion intensity for the current image
intensity2 number No 1 Distortion intensity for the next image
angle1 number No Math.PI / 4 Rotation angle for the current image
angle2 number No — Rotation angle for the next image
speedIn number No 1.2 Duration (seconds) of the forward transition
speedOut number No 1.0 Duration (seconds) of the backward transition
hover boolean No false Advance to next image on hover
easing string No 'expo.out' GSAP easing string
distortionClass string No '' CSS class applied to the canvas container element

Displacement maps

The displacementImage prop is optional. When omitted, the component uses a built-in displacement map served from jsDelivr CDN — no setup required.

The package ships 16 displacement textures. You can reference any of them via the exported DISPLACEMENT_URLS array:

import { DISPLACEMENT_URLS } from 'vue-hover-effect'
// DISPLACEMENT_URLS[0]  →  map #1  (default)
// DISPLACEMENT_URLS[5]  →  map #6
// ...up to DISPLACEMENT_URLS[15]
<template>
  <SingleHoverEffect
    image1="/images/photo-a.webp"
    image2="/images/photo-b.webp"
    :displacement-image="DISPLACEMENT_URLS[4]"
  />
</template>

Host displacement maps yourself

If you prefer to serve the files locally (offline support, no CDN dependency), copy them from the package to your project's public folder:

cp -r node_modules/vue-hover-effect/dist/lib-images/displacements public/displacements

Then pass the local path:

<SingleHoverEffect
  image1="/images/photo-a.webp"
  image2="/images/photo-b.webp"
  displacement-image="/displacements/3.webp"
/>

TypeScript

All prop types are exported and can be imported for use in your own components or composables:

import type {
  SingleHoverEffectProps,
  MultiImageEffectProps,
  SingleHoverEffectOptions,
  MultiImageEffectOptions,
  SingleHoverEffectController,
  MultiImageEffectController,
} from 'vue-hover-effect'

SingleHoverEffectProps and MultiImageEffectProps are the component-facing types (without parent, which is handled internally). SingleHoverEffectOptions / MultiImageEffectOptions are the full option types accepted by the underlying factory functions.


Nuxt

The components are SSR-safe out of the box. Three.js and GSAP are dynamically imported inside onMounted, so they are never included in the server bundle and no WebGL code runs during server-side rendering.

No extra configuration is needed. Just import and use:

<!-- pages/index.vue -->
<script setup lang="ts">
import { SingleHoverEffect } from 'vue-hover-effect'
</script>

<template>
  <SingleHoverEffect
    image1="/images/photo-a.webp"
    image2="/images/photo-b.webp"
    displacement-image="/displacements/displacement.webp"
  />
</template>

If you prefer an explicit client-only guarantee, Nuxt's built-in <ClientOnly> wrapper also works:

<template>
  <ClientOnly>
    <SingleHoverEffect
      image1="/images/photo-a.webp"
      image2="/images/photo-b.webp"
      displacement-image="/displacements/displacement.webp"
      :hover="true"
    />
  </ClientOnly>
</template>

Global registration

You can register both components globally in your Vue app:

// main.ts
import { createApp } from 'vue'
import { SingleHoverEffect, MultipleHoverEffect } from 'vue-hover-effect'
import App from './App.vue'

const app = createApp(App)
app.component('SingleHoverEffect', SingleHoverEffect)
app.component('MultipleHoverEffect', MultipleHoverEffect)
app.mount('#app')

For Nuxt, register them in a plugin:

// plugins/vue-hover-effect.client.ts
import { SingleHoverEffect, MultipleHoverEffect } from 'vue-hover-effect'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('SingleHoverEffect', SingleHoverEffect)
  nuxtApp.vueApp.component('MultipleHoverEffect', MultipleHoverEffect)
})

Using the factory functions directly

The underlying factory functions are also exported for headless usage without the Vue components:

import { createSingleHoverEffect, createMultiImageEffect } from 'vue-hover-effect'

const controller = createSingleHoverEffect({
  parent: document.querySelector('#my-container') as HTMLElement,
  image1: '/images/photo-a.webp',
  image2: '/images/photo-b.webp',
  displacementImage: '/displacements/displacement.webp',
  speedIn: 1.2,
  speedOut: 0.9,
  hover: true,
})

// Manual control
controller?.next()     // transition to image2
controller?.previous() // transition back to image1
controller?.resize()   // recalculate on container resize

License

MIT — Sultonkhon

Related Packages

vuejs-typing

VueJs-Typing Vue Typing is a lightweight directive compatible with both Vue 2 an...

1,445 0
@scayle/storefront-nuxt

Nuxt integration for the SCAYLE Commerce Engine and Storefront API

152,903
nuxt-tsconfig-stub

Stub Nuxt's generated tsconfig.json at installation

70,835 24
kuranuxt

Look at the Nuxt 3 documentation to learn more.

871 0
nuxt-define

One utility function for module authors to define compiler constants for all bui...

13,412,511

Version History

Version Released Vue License
0.0.8-stable.4 ^3.4.37
0.0.8-stable.3 ^3.4.37
0.0.8-stable.2 ^3.4.37
0.0.8-stable.1 ^3.4.37
0.0.8-stable.0 ^3.4.37
Pre-releases (28)
Version Released Vue License
0.1.0-alpha ^3.5.13 MIT
0.0.9-alpha.8 ^3.5.13 MIT
0.0.9-alpha.7 ^3.4.37 MIT
0.0.9-alpha.6 ^3.4.37 MIT
0.0.9-alpha.5 ^3.4.37