How to Make a Logo That Follows the Cursor
By the Charmotion team · Updated

To make a logo follow the cursor, listen for pointermove, turn the pointer's position relative to the logo into a small angle or offset, and ease the logo toward it. Use CSS transforms for a tilt, Math.atan2 for eyes that look at the pointer, or Rive listeners and data binding for a designed rig. Turn it off for reduced motion.
Key takeaways
- CSS alone can react to hover but cannot read the pointer's position; a few lines of JavaScript set CSS variables for tilt.
- Math.atan2(y, x) gives the angle from the eye to the pointer; move each pupil a fixed distance along it.
- Rive listeners react to the pointer over the canvas; to follow it across the whole page, pass values in through data binding.
- Touch screens have no hover, so plan an idle motion or a tap reaction instead.
- Motion triggered by the pointer should switch off with prefers-reduced-motion (WCAG 2.3.3), and long idle loops need a way to stop (WCAG 2.2.2).
Already have a character or logo? See examples in real app screens: splash, onboarding, loading and empty-state screens, with the animation that plays in each.
What are the ways a logo can react to the mouse?
| Effect | What moves | Simplest tool |
|---|---|---|
| Hover reaction | The logo plays a short move when the pointer is over it | CSS :hover |
| Tilt toward the cursor | The whole logo turns a few degrees toward the pointer | JavaScript plus CSS transforms |
| Eyes follow | Pupils, or one part of the logo, point at the cursor | JavaScript with Math.atan2 on an inline SVG |
| Designed rig | Several parts respond with easing, squash and states | Rive (listeners and data binding) |
| 3D turn | A 3D model rotates in space | three.js |
For a 3D model, see an interactive 3D logo on your website. The rest of this guide covers the 2D options.
Can you make a logo react to the mouse with CSS only?
Partly. CSS can react to whether the pointer is over the logo, but not where it is. A hover reaction is pure CSS:
.logo { transition: transform 0.25s ease-out; }
@media (hover: hover) {
.logo:hover { transform: rotate(-6deg) scale(1.05); }
}
The hover media feature tests whether "the user's primary input mechanism can hover over elements." On many phones it reports none, because they "emulate hovering when the user performs an inconvenient long tap." Wrapping hover effects in @media (hover: hover) keeps them to devices whose main input can actually hover.
To tilt toward the pointer, you need its coordinates, which means a small script.
How do you tilt a logo toward the cursor with JavaScript?
Measure where the pointer is relative to the logo's center, convert that to an angle, and hand it to CSS through custom properties. The CSS transition smooths the motion, so there is no animation loop to manage.
<a class="logo-link" href="/"><img class="logo" src="/logo.svg" alt="Hushwave"></a>
.logo {
transform: perspective(600px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
transition: transform 0.2s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.logo { transform: none; transition: none; }
}
const logo = document.querySelector(".logo");
const moves = matchMedia("(prefers-reduced-motion: no-preference) and (hover: hover)");
if (moves.matches) {
window.addEventListener("pointermove", (e) => {
const r = logo.getBoundingClientRect();
const x = (e.clientX - (r.left + r.width / 2)) / innerWidth; // about -1 to 1
const y = (e.clientY - (r.top + r.height / 2)) / innerHeight;
logo.style.setProperty("--ry", (x * 20) + "deg"); // flip the signs to tilt away instead
logo.style.setProperty("--rx", (-y * 20) + "deg");
});
}
Keep the angles small. A logo that swings 45 degrees reads as broken, while a few degrees reads as alive. If the logo sits in a header that stays put, measure it once on resize instead of on every event.
Listening on window makes the logo react anywhere on the page. Listen on the header element instead if it should only react nearby.
How do you make a logo's eyes follow the cursor?
The logo has to be an inline SVG with the pupils as separate elements, so you can move them. Then, for each eye, find the angle from the eye's center to the pointer with Math.atan2, which returns "the angle in radians (between -π and π, inclusive)" and takes "the y-coordinate first and the x-coordinate second," per MDN.
const eye = document.querySelector("#eye-left");
const pupil = document.querySelector("#pupil-left");
const reach = 3; // how far the pupil may travel, in SVG units
window.addEventListener("pointermove", (e) => {
const r = eye.getBoundingClientRect();
const a = Math.atan2(e.clientY - (r.top + r.height / 2), e.clientX - (r.left + r.width / 2));
pupil.setAttribute("transform", "translate(" + Math.cos(a) * reach + " " + Math.sin(a) * reach + ")");
});
A fixed reach keeps the pupil inside the eye whatever the distance. For a softer look, scale the reach down when the pointer is very close to the eye, so the pupil does not snap to the edge.
When exporting from Figma, the SVG option Include "id" attribute adds ids based on layer names, so naming layers eye-left and pupil-left gives the script something to find.
How do you make a Rive logo follow the pointer?
Rive gives you two routes, and the right one depends on how far the pointer should be followed.
Listeners, inside the canvas. A state machine listener can fire on Pointer Enter, Exit, Move, Down and Up. Its Align Target action "positions an object so it follows the pointer," for example an invisible target that the eyes or head are constrained to. Rive's docs warn that Pointer Exit "only fires while Rive can read the pointer position," so listeners see the pointer only over the canvas. The older Input Change action is marked deprecated.
Data binding, across the page. Data binding "connects data stored in View Models to properties in your Rive scene," and values can be updated from runtime code. Listen for the pointer on the page and write numbers into the file:
const r = new rive.Rive({
src: "/logo.riv",
canvas: document.querySelector("#logo"),
stateMachine: "Main",
autoplay: true,
autoBind: true,
isTouchScrollEnabled: true,
onLoad: () => {
const lookX = r.viewModelInstance.number("lookX"); // a number property defined in your file
window.addEventListener("pointermove", (e) => {
lookX.value = (e.clientX / innerWidth) * 100;
});
},
});
isTouchScrollEnabled keeps the page scrollable when a finger drags across the canvas; without it, Rive says "scroll behavior may be prevented." The animation side (blend states driven by the number) is set up in the editor; the Rive state machine guide explains how, and SVG to Rive covers getting the logo into Rive.
What happens on phones and tablets without a mouse?
Touch screens have no resting pointer. MDN's pointer events docs describe enter and leave events on devices that "do not support hover" arriving together with the press and release, so a cursor-following logo only moves while a finger is on the screen, then freezes wherever it was.
- Gate the effect: run cursor following only under
(hover: hover), as in the tilt example. - Give touch its own motion: a slow idle loop, or a short reaction on tap, works better than a half-working follow.
- Do not block scrolling: MDN describes
touch-action: noneas disabling the browser's default touch behavior, such as panning and zooming, for that region. On a logo in the page flow, a drag that starts on it would no longer scroll the page. - Reset on release: ease back to the resting pose on
pointerupandpointercancel, so the logo does not stay twisted.
Is a logo that follows the cursor accessible?
It can be, if people can turn the motion off. Two WCAG criteria apply:
| Criterion | Level | What it asks |
|---|---|---|
| 2.3.3 Animation from Interactions | AAA | "Motion animation triggered by interaction can be disabled," unless it is essential |
| 2.2.2 Pause, Stop, Hide | A | Moving content that "starts automatically, lasts more than five seconds, and is presented in parallel with other content" needs a way to pause, stop or hide it |
A cursor-following logo is motion triggered by interaction, and it is never essential. W3C lists the prefers-reduced-motion media query as a sufficient technique (C39) for 2.3.3, which is why every example above checks it. If the logo also has an idle loop that keeps playing, stop it after a few cycles or give it a pause control to meet 2.2.2.
Also keep the logo a normal link with alt text. The motion is decoration; the brand name and the link to the home page are the content. More in accessible animation and reduced motion.
Can you get a logo that follows the pointer ready-made?
Charmotion's Motion Studio builds a website hero from your own logo that follows the pointer, reacts on hover and loops while idle. It works from your file's own parts, so the logo is not redrawn. You get a hosted embed that pauses off screen and respects reduced motion, self-host files and a React component. Concepts are free; building one starts at Starter ($9/month as of November 2026). For a logo with a face that should blink and talk, a character set is the better fit.
Frequently asked questions
Does a cursor-following logo slow down the page?
Not if it stays small. A pointermove handler that sets two CSS variables is cheap. Avoid measuring layout on every event when the logo does not move, pause any animation loop while the logo is off screen, and skip the effect entirely on touch devices and for reduced motion.
Can a Rive logo follow the cursor outside its canvas?
Not with listeners alone, which only see the pointer over the canvas. Listen for pointermove on the page in JavaScript and write the values into a view model number property through data binding; the state machine then turns those numbers into motion.
How far should a logo tilt toward the cursor?
A few degrees is enough to feel responsive without looking broken. Large angles distort the logo and make text hard to read. Ease the motion over a fraction of a second rather than snapping, and return to the resting pose when the pointer leaves.
Sources
- Pointer events, MDN Web Docs
- hover media feature, MDN Web Docs
- Math.atan2(), MDN Web Docs
- Window: requestAnimationFrame() method, MDN Web Docs
- prefers-reduced-motion, MDN Web Docs
- Listeners, Rive docs
- Data binding overview, Rive docs
- Web data binding, Rive docs
- Rive parameters (web runtime), Rive docs
- Understanding SC 2.3.3: Animation from Interactions, W3C WAI
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- C39: Using the CSS prefers-reduced-motion query to prevent motion, W3C WAI
- Export formats and settings for static designs, Figma Learn