@8bitbish/flux-hover
v0.3.1
Published
Premium directional magnetic hover background effect for React
Maintainers
Readme
FluxHover
A directional magnetic hover background effect for React. The highlight enters from the direction the cursor approached, follows the cursor softly while hovered, and exits toward where the cursor leaves.
Built with React, TypeScript, and Framer Motion.
Install
npm install @8bitbish/flux-hover framer-motion
react,react-dom, andframer-motionare peer dependencies.
Quick start
import { FluxHover } from '@8bitbish/flux-hover';
function NavButton() {
return (
<FluxHover background="rgba(255,255,255,0.08)" style={{ borderRadius: 10 }}>
<button>Features</button>
</FluxHover>
);
}The blob automatically inherits the borderRadius of the wrapper element — set it once via style or a CSS class and you're done.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Content to wrap |
| background | string | "rgba(255,255,255,0.09)" | CSS background of the hover blob (color, gradient, etc.) |
| pressColor | string | — | Color overlaid on the blob on pointer-down. Accepts any CSS color including rgba. |
| intensity | number | 0.25 | Magnetic movement strength 0–1. Higher = more travel |
| duration | number | 0.22 | Enter/exit animation duration in seconds |
| opacity | number | 1 | Peak opacity of the blob 0–1 |
| scale | number | 1 | Scale of the blob at rest |
| enterDelay | number | 40 | Delay in ms before hover activates. Cancels if cursor leaves first — useful for segmented controls |
| clip | boolean | false | Clip blob to wrapper bounds (overflow: hidden). Blob slides in from the entry edge |
| focusVisible | boolean | true | Show blob on :focus-visible (keyboard nav) |
| disabled | boolean | false | Disable all hover and press effects |
| as | keyof JSX.IntrinsicElements | "div" | Render wrapper as a different element |
| className | string | — | Extra class on the wrapper element |
| style | CSSProperties | — | Inline styles on the wrapper element. Use this to set borderRadius. |
Any extra props (e.g. href, onClick) are forwarded to the underlying element.
Examples
Ghost button
<FluxHover
background="rgba(255,255,255,0.07)"
pressColor="rgba(255,255,255,0.12)"
style={{ borderRadius: 10 }}
>
<button>Click me</button>
</FluxHover>Tinted button
<FluxHover
background="rgba(99,102,241,0.22)"
pressColor="rgba(99,102,241,0.2)"
enterDelay={0}
style={{ borderRadius: 10 }}
>
<button style={{ color: '#a5b4fc' }}>Get started</button>
</FluxHover>Segmented control
<div style={{ display: 'inline-flex', gap: 4, padding: 4, background: '#111', borderRadius: 11, overflow: 'hidden' }}>
{['All', 'Design', 'Engineering'].map((tab) => (
<FluxHover key={tab} style={{ borderRadius: 7 }}>
<button>{tab}</button>
</FluxHover>
))}
</div>Card hover
<FluxHover
as="article"
background="rgba(99,102,241,0.08)"
intensity={0.4}
enterDelay={0}
style={{ padding: 24, border: '1px solid #1e1e1e', borderRadius: 16 }}
>
<h3>Card title</h3>
<p>Some content</p>
</FluxHover>FluxHoverProvider
Set project-wide defaults once. Every FluxHover in the tree inherits them, and any prop passed directly to an instance overrides the default.
import { FluxHoverProvider, FluxHover } from '@8bitbish/flux-hover';
<FluxHoverProvider defaults={{
background: "rgba(255,255,255,0.09)",
pressColor: "rgba(255,255,255,0.15)",
duration: 0.22,
intensity: 0.25,
enterDelay: 40,
}}>
<App />
</FluxHoverProvider>
// Inherits all provider defaults
<FluxHover style={{ borderRadius: 7 }}>
<button>Tab</button>
</FluxHover>
// Overrides just what differs
<FluxHover background="rgba(99,102,241,0.22)" enterDelay={0} style={{ borderRadius: 10 }}>
<button>Get started</button>
</FluxHover>useFluxHover hook
For lower-level control, the hook is exported directly:
import { useFluxHover } from '@8bitbish/flux-hover';
import { motion, useTransform } from 'framer-motion';
function CustomButton() {
const {
wrapperRef,
blobX, blobY,
blobOpacity, blobScale,
pressScale, pressOverlay,
onPointerEnter, onPointerMove, onPointerLeave,
onPointerDown, onPointerUp,
onFocus, onBlur,
} = useFluxHover({ intensity: 0.25, duration: 0.22 });
const combinedScale = useTransform(() => blobScale.get() * pressScale.get());
return (
<div
ref={wrapperRef}
style={{ position: 'relative', display: 'inline-flex', borderRadius: 8 }}
onPointerEnter={onPointerEnter}
onPointerMove={onPointerMove}
onPointerLeave={onPointerLeave}
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onFocus={onFocus}
onBlur={onBlur}
>
<motion.span
aria-hidden
style={{
position: 'absolute', inset: 0,
borderRadius: 'inherit',
background: 'rgba(255,255,255,0.09)',
overflow: 'hidden',
pointerEvents: 'none',
x: blobX, y: blobY,
opacity: blobOpacity,
scale: combinedScale,
}}
>
<motion.span
aria-hidden
style={{
position: 'absolute', inset: 0,
background: 'rgba(255,255,255,0.15)',
opacity: pressOverlay,
}}
/>
</motion.span>
<span style={{ position: 'relative', zIndex: 1 }}>
Custom content
</span>
</div>
);
}Customization notes
Subtle default feel — defaults are tuned for modern dark UIs. Small background opacity (0.07–0.12) and moderate intensity (0.25) produce the most polished result.
Segmented controls — use enterDelay={40} to debounce fast sweeps, preventing the effect from firing when the cursor briefly crosses an item.
Standalone buttons — pass enterDelay={0} to override the default delay for elements that should respond instantly.
Gradient backgrounds — any CSS background value works:
background="linear-gradient(135deg, rgba(99,102,241,0.25), rgba(168,85,247,0.25))"Reduced motion — FluxHover automatically detects prefers-reduced-motion and skips all animations, falling back to an instant appear/disappear.
Focus visible — keyboard users get the same hover highlight on :focus-visible. Pass focusVisible={false} to opt out.
License
MIT
