Last active
January 19, 2025 13:23
-
-
Save jaredloson/3f0142f1040470b4f83d2e495a7ce5fd to your computer and use it in GitHub Desktop.
Interactive Javascript Cursor in React
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* { | |
cursor: none !important; | |
} | |
.show-cursor { | |
cursor: auto !important; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const isTouchDevice = | |
"ontouchstart" in window | |
|| navigator.MaxTouchPoints > 0 | |
|| navigator.msMaxTouchPoints > 0; | |
export default isTouchDevice; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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