Skip to content

Instantly share code, notes, and snippets.

@fibonacid
Last active May 27, 2024 08:09
Show Gist options
  • Save fibonacid/f52524c02debe4c89603614b30a5d4ae to your computer and use it in GitHub Desktop.
Save fibonacid/f52524c02debe4c89603614b30a5d4ae to your computer and use it in GitHub Desktop.
Mouse Cursor (React + GSAP)
import { Container } from "./styles";
import {
forwardRef,
PropsWithChildren,
useImperativeHandle,
useRef,
} from "react";
export type CursorProps = PropsWithChildren<{
className?: string;
width: number;
height: number;
}>;
export type CursorRef = {
moveTo: (x: number, y: number) => void;
} | null;
const styles = {
position: "absolute",
top: "-1000px",
left: "-1000px"
}
/**
* Renders an element that exposes an imperative API to move it around
* @example @/components/Cursor/Example.tsx
*/
const Cursor = forwardRef<CursorRef, CursorProps>(function Cursor(props, ref) {
const { className, width, height, children } = props;
const container = useRef<HTMLSpanElement>(null);
useImperativeHandle(ref, () => ({
moveTo(x, y) {
gsap.to(container.current, {
x: x - width * 0.5,
y: y - height * 0.5,
});
},
}));
return (
<div style={styles} ref={container} className={className}>
{children}
</div>
);
});
export default Cursor;
type IconProps = {
width: number;
height: number;
};
const Icon: React.FC<IconProps> = (props) => {
const { width, height } = props;
// Cat and Mouse, lol
return <span style={{ width, height }}>🐈</span>;
};
import Cursor, { CursorRef } from "./Cursor.tsx";
import Icon from "./Icon.tsx";
import { useRef } from "react";
// Knowing width and height ahead of time improves performance
// because we can skip calling `element.getBoundingClientRect()`
const WIDTH = 40;
const HEIGHT = 40;
export const MouseCursor: React.FC = () => {
const ref = useRef<CursorRef>(null);
useEffect(() => {
// Enclose ref value under the current scope.
const api = ref.current;
if (api) {
const handleMouseMove = (event: MouseEvent) => {
// Call api to move the element based on mouse coordinates
api.moveTo(event.clientX, event.clientY);
};
// Listen for mouse movement throught the whole document.
document.addEventListener("mousemove", handleMouseMove);
return () => {
// Cancel listener when component unmounts
document.removeEventListener("mousemove", handleMouseMove);
};
}
}, []);
return (
<Cursor ref={ref} width={WIDTH} height={HEIGHT}>
<Icon width={WIDTH} height={HEIGHT} />
</Cursor>
);
};
import Cursor, { CursorRef } from "./Cursor.tsx";
import Icon from "./Icon.tsx";
import { useRef } from "react";
// Knowing width and height ahead of time improves performance
// because we can skip calling `element.getBoundingClientRect()`
const WIDTH = 40;
const HEIGHT = 40;
const styles = {
width: "500px",
height: "500px"
}
/**
* Same thing but movement is restricted to a specific area.
*/
export const MouseCursorWithBounds: React.FC = () => {
const ref = useRef<CursorRef>(null);
const handleMouseMove = useCallback<MouseEventHandler<HTMLDivElement>>(
(event) => {
const api = ref.current;
if (api) {
// Call api to move the element based on mouse coordinates
api.moveTo(event.clientX, event.clientY);
}
},
[]
);
return (
<div style={styles} onMouseMove={handleMouseMove}>
<Cursor ref={ref} width={WIDTH} height={HEIGHT}>
<Icon width={WIDTH} height={HEIGHT} />
</Cursor>
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment