Skip to content

Instantly share code, notes, and snippets.

@jaredloson
Last active December 3, 2022 19:35
Show Gist options
  • Save jaredloson/3f0142f1040470b4f83d2e495a7ce5fd to your computer and use it in GitHub Desktop.
Save jaredloson/3f0142f1040470b4f83d2e495a7ce5fd to your computer and use it in GitHub Desktop.
Interactive Javascript Cursor in React
* {
cursor: none !important;
}
.show-cursor {
cursor: auto !important;
}
import React, { useContext, useState } from "react";
import useMousePosition from "./useMousePosition";
import { CursorContext } from "./CursorContextProvider";
import isTouchDevice from "./isTouchDevice";
const Cursor = () => {
if (isTouchDevice) {
return null;
}
const { clientX, clientY } = useMousePosition();
const [cursor] = useContext(CursorContext);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleMouseEnter = () => setIsVisible(true);
const handleMouseLeave = () => setIsVisible(false);
document.body.addEventListener("mouseenter", handleMouseEnter);
document.body.addEventListener("mouseleave", handleMouseLeave);
return () => {
document.body.removeEventListener("mouseenter", handleMouseEnter);
document.body.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
return (
<div
style={{
position: "fixed",
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 9999,
pointerEvents: "none"
}}
>
<svg
width={50}
height={50}
viewBox="0 0 50 50"
style={{
position: "absolute",
pointerEvents: "none",
left: clientX,
top: clientY,
transform: `translate(-50%, -50%) scale(${cursor.active ? 2.5 : 1})`,
stroke: cursor.active ? "black" : "white",
strokeWidth: 1,
fill: cursor.active ? "rgba(255,255,255,.5)" : "black",
transition: "transform .2s ease-in-out",
// TODO: extra check on clientX needed here
// because mouseleave event not always firing
// when slowly exiting left side of browser
opacity: isVisible && clientX > 1 ? 1 : 0,
}}
>
<circle
cx="25"
cy="25"
r="8"
/>
</svg>
</div>
);
};
import React, { createContext, useState } from "react";
export const CursorContext = createContext();
const CursorContextProvider = () => {
const [cursor, setCursor] = useState({ active: false });
return (
<CursorContext.Provider value={[cursor, setCursor]}>
{children}
</CursorContext.Provider>
);
};
export default CursorContextProvider;
const Button = () => {
const cursorHandlers = useCursorHandlers();
return (
<button type="button" style={{ padding: "1rem" }} {...cursorHandlers}>
HOVER ME
</button>
)
};
const Select = () => {
return (
<select class="show-cursor">
{Array(5).fill().map((item, idx) =>
<option value={idx}>Option {idx + 1}</option>
)}
</select>
)
};
const App = () => {
return (
<CursorContextProvider>
<Cursor />
<Button />
<br />
<Select />
</CursorContextProvider>
);
};
const isTouchDevice =
"ontouchstart" in window
|| navigator.MaxTouchPoints > 0
|| navigator.msMaxTouchPoints > 0;
export default isTouchDevice;
import { useContext, useCallback } from "react";
import { CursorContext } from "./CursorContextProvider";
import isTouchDevice from "./isTouchDevice";
const useCursorHandlers = (options = {}) => {
if (isTouchDevice) {
return options;
}
const [, setCursor] = useContext(CursorContext);
const toggleCursor = () => {
setCursor(({ active }) => ({ active: !active }));
};
const onMouseEnter = useCallback(event => {
if (options.onMouseEnter) {
options.onMouseEnter(event);
}
toggleCursor();
});
const onMouseLeave = useCallback(event => {
if (options.onMouseLeave) {
options.onMouseLeave(event);
}
toggleCursor();
});
return { onMouseEnter, onMouseLeave };
};
import { useState, useEffect } from "react";
const useMousePosition = () => {
const [position, setPosition] = useState({
clientX: 0,
clientY: 0,
});
const updatePosition = event => {
const { pageX, pageY, clientX, clientY } = event;
setPosition({
clientX,
clientY,
});
};
useEffect(() => {
document.addEventListener("mousemove", updatePosition, false);
document.addEventListener("mouseenter", updatePosition, false);
return () => {
document.removeEventListener("mousemove", updatePosition);
document.removeEventListener("mouseenter", updatePosition);
};
}, []);
return position;
};
export default useMousePosition;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment