vue-hover-effect
v0.0.1-stable
Published
> WebGL-powered image hover transition effects for Vue 3, built on top of [Three.js](https://threejs.org/) and [GSAP](https://gsap.com/). Inspired by [Robin Delaporte's hover-effect](https://github.com/robin-dela/hover-effect).
Maintainers
Readme
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/previousslot 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-effectPeer dependencies
Three.js and GSAP are peer dependencies. Install them alongside the package:
npm install three gsapUsage
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/displacementsThen 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 resizeLicense
MIT — Sultonkhon
