Skip to content

wfMaxim/hover-repel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hover Repel

Hover Repel demo

A tiny hover interaction library that makes elements react to your cursor with a quick directional “repel” movement.

Hover Repel pushes an element away based on the direction and speed of your pointer, then smoothly returns it to its original position. It is great for buttons, cards, icons, badges, product items, playful UI details, and landing pages.


Features

  • Direction-aware hover movement
  • Speed-based movement strength
  • Optional rotation/tilt effect
  • Works with vanilla HTML/JS, Vue, Nuxt, React, Astro, and more
  • Simple data-repel API
  • Vue directive support with v-repel
  • Optional stable hitbox with data-repel-container
  • Automatically creates a wrapper when needed
  • Lightweight core
  • TypeScript support
  • Reduced-motion friendly

Installation

npm install hover-repel
pnpm add hover-repel
yarn add hover-repel

Vanilla HTML / JavaScript

Use the browser build from a CDN.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hover-repel/dist/style.css">

<div data-repel class="card">
  Hover me
</div>

<script src="https://cdn.jsdelivr.net/npm/hover-repel/dist/browser.global.js"></script>
<script>
  HoverRepel.repel();
</script>

Example page:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hover Repel Demo</title>

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hover-repel/dist/style.css">

  <style>
    body {
      min-height: 100vh;
      display: grid;
      place-items: center;
      background: #111;
    }

    .card {
      width: 180px;
      height: 240px;
      border-radius: 24px;
      background: #31e981;
    }
  </style>
</head>
<body>
  <div data-repel class="card"></div>

  <script src="https://cdn.jsdelivr.net/npm/hover-repel/dist/browser.global.js"></script>
  <script>
    HoverRepel.repel();
  </script>
</body>
</html>

You can also use local files:

<link rel="stylesheet" href="./style.css">

<div data-repel class="card"></div>

<script src="./browser.global.js"></script>
<script>
  HoverRepel.repel();
</script>

Bundler Usage

For Vite, Webpack, Parcel, Astro, React, Vue, Nuxt, and other bundlers:

import { repel } from "hover-repel";
import "hover-repel/style.css";

repel();

Basic markup:

<div data-repel class="card">
  Hover me
</div>

How It Works

Hover Repel uses a moving element and an optional stable hitbox.

<div data-repel-container>
  <div data-repel></div>
</div>

data-repel-container is the stable hover area.
data-repel is the element that visually moves.

For most cases, you only need this:

<div data-repel></div>

Hover Repel will automatically create a wrapper when needed.

Use a manual container when you want full control over the hitbox, spacing, or layout.


Vue

Register the Vue plugin once:

// main.ts
import { createApp } from "vue";
import App from "./App.vue";

import HoverRepelVue from "hover-repel/vue";
import "hover-repel/style.css";

const app = createApp(App);

app.use(HoverRepelVue);

app.mount("#app");

Then use v-repel anywhere:

<template>
  <button v-repel class="button">
    Hover me
  </button>
</template>

With options:

<template>
  <button
    v-repel="{
      maxStrength: 100,
      maxRotate: 35,
      resetDelay: 250
    }"
  >
    Hover me harder
  </button>
</template>

Global defaults:

app.use(HoverRepelVue, {
  maxStrength: 90,
  maxRotate: 30,
  resetDelay: 250
});

Local directive usage:

<script setup lang="ts">
import { vRepel } from "hover-repel/vue";
import "hover-repel/style.css";
</script>

<template>
  <button v-repel>
    Hover me
  </button>
</template>

Nuxt

Add the CSS globally:

// nuxt.config.ts
export default defineNuxtConfig({
  css: ["hover-repel/style.css"]
});

Create a client plugin:

// plugins/hover-repel.client.ts
import HoverRepelVue from "hover-repel/vue";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(HoverRepelVue);
});

Use it in any component:

<template>
  <NuxtLink v-repel to="/contact" class="cta">
    Contact us
  </NuxtLink>
</template>

With global defaults:

// plugins/hover-repel.client.ts
import HoverRepelVue from "hover-repel/vue";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(HoverRepelVue, {
    maxStrength: 90,
    maxRotate: 30,
    resetDelay: 250
  });
});

React

React can use the vanilla API.

import { useEffect, useRef } from "react";
import { attachRepel } from "hover-repel";
import "hover-repel/style.css";

export function Card() {
  const ref = useRef(null);

  useEffect(() => {
    if (!ref.current) return;

    const destroy = attachRepel(ref.current);

    return () => destroy();
  }, []);

  return (
    <div ref={ref} data-repel className="card">
      Hover me
    </div>
  );
}

Data Attributes

Attribute Description Default
data-repel Element that should move Required for vanilla usage
data-repel-container Stable hover area Optional
data-repel-strength Maximum movement distance in pixels 75
data-repel-rotate Maximum rotation in degrees 26
data-repel-reset Delay before returning to rest in milliseconds 300

Example:

<div
  data-repel
  data-repel-strength="90"
  data-repel-rotate="30"
  data-repel-reset="250"
>
  Hover me
</div>

In Vue/Nuxt, v-repel automatically adds data-repel.


JavaScript API

repel(options?)

Initializes all matching repel elements.

import { repel } from "hover-repel";

const destroy = repel();

With options:

const destroy = repel({
  root: document.querySelector(".section"),
  maxStrength: 90,
  maxRotate: 30,
  resetDelay: 250
});

Cleanup:

destroy();

attachRepel(element, options?)

Attaches the effect to one element.

import { attachRepel } from "hover-repel";

const element = document.querySelector(".card");
const destroy = attachRepel(element);

This is useful for components and dynamic elements.


Options

type RepelOptions = {
  minStrength?: number;
  maxStrength?: number;
  speedMultiplier?: number;

  minRotate?: number;
  maxRotate?: number;
  rotateMultiplier?: number;

  resetDelay?: number;

  containerSelector?: string;
  autoWrap?: boolean;
};

For repel() you can also pass:

type RepelInitOptions = RepelOptions & {
  root?: ParentNode;
  targetSelector?: string;
};
Option Description Default
root Root used by repel() to find targets document
targetSelector Selector used by repel() [data-repel]
containerSelector Selector for the stable hover container [data-repel-container]
autoWrap Automatically creates a wrapper when no container exists true
minStrength Minimum movement distance 18
maxStrength Maximum movement distance 75
speedMultiplier How much pointer speed affects movement 130
minRotate Minimum rotation amount 8
maxRotate Maximum rotation amount 26
rotateMultiplier How much pointer speed affects rotation 55
resetDelay Delay before returning to rest 300

CSS Variables

Hover Repel uses CSS variables for the transform:

[data-repel] {
  --repel-x: 0px;
  --repel-y: 0px;
  --repel-r: 0deg;
}

Customize animation timing:

[data-repel] {
  --repel-duration: 900ms;
  --repel-ease: cubic-bezier(.16, 1.8, .35, 1);
}

Styling Example

.card {
  width: 220px;
  height: 280px;
  border-radius: 2rem;
  background: linear-gradient(135deg, #31e981, #16c7ff);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.25);
}
<div data-repel class="card"></div>

Package Exports

import { repel, attachRepel } from "hover-repel";
import HoverRepelVue, { vRepel } from "hover-repel/vue";
import "hover-repel/style.css";

For vanilla HTML:

<script src="https://cdn.jsdelivr.net/npm/hover-repel/dist/browser.global.js"></script>
<script>
  HoverRepel.repel();
</script>

Common Issues

The effect does not work

Make sure the CSS is loaded:

import "hover-repel/style.css";

or in plain HTML:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hover-repel/dist/style.css">

Nuxt only works on first load

Use the Nuxt client plugin with v-repel instead of manually calling repel().

// plugins/hover-repel.client.ts
import HoverRepelVue from "hover-repel/vue";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(HoverRepelVue);
});

The layout changes unexpectedly

Hover Repel automatically creates a wrapper when no data-repel-container exists.

For full layout control:

<div data-repel-container>
  <div data-repel></div>
</div>

Or disable auto wrapping:

repel({
  autoWrap: false
});

Browser Support

Hover Repel uses modern pointer events and CSS transforms. It works in all modern browsers.


License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors