Custom Cursor Animation at 60 fps: Complete Implementation Guide

Custom Cursor Animation at 60 fps: Complete Implementation Guide

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1419
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    983
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1248
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    984
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    998

Custom Cursor Animation at 60 fps: Complete Implementation Guide

The visitor moves the mouse, but the cursor jerks, lags, or disappears — familiar? Most often the problem is lack of proper optimization. We've analyzed projects where developers used jQuery animation, which caused lag on mobile devices. The correct approach is using requestAnimationFrame and CSS will-change: transform. At 60 fps, the follower delay with lerp 0.12 is about 8-10 ms — imperceptible to the eye.

We develop custom animated cursors for websites — from simple trailing effects to complex states with interface adaptation. With over 5 years of experience and 20+ successful cursor projects, we guarantee no flickering and consistent 60fps. A poor implementation reveals itself through lag, stuttering, and conflicts with native browser states. Our team, experienced in interactive interfaces, ensures the cursor works seamlessly and enhances your project's character. Get an engineer consultation — write to us, we'll evaluate your project in 1 day.

Anatomy and Movement Logic

Typical structure: two elements — a dot that follows the mouse without delay, and a follower circle that trails with a lerp effect. Instead of cursor: none on the entire document, we hide it only where needed.

* { cursor: none; } .cursor-dot { position: fixed; width: 8px; height: 8px; border-radius: 50%; background: #fff; pointer-events: none; z-index: 9999; transform: translate(-50%, -50%); will-change: transform; } .cursor-ring { position: fixed; width: 36px; height: 36px; border-radius: 50%; border: 1.5px solid rgba(255, 255, 255, 0.6); pointer-events: none; z-index: 9998; transform: translate(-50%, -50%); will-change: transform; } 

Animation via requestAnimationFrame with linear interpolation for the follower. The dot position updates directly on mousemove — no lerp, otherwise precision is lost.

interface CursorState { mouse: { x: number; y: number } follower: { x: number; y: number } isHovering: boolean isVisible: boolean } class CustomCursor { private dot: HTMLElement private ring: HTMLElement private state: CursorState private rafId: number | null = null private readonly LERP = 0.12 constructor() { this.dot = document.querySelector('.cursor-dot')! this.ring = document.querySelector('.cursor-ring')! this.state = { mouse: { x: -100, y: -100 }, follower: { x: -100, y: -100 }, isHovering: false, isVisible: false, } this.init() } private init() { document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mouseenter', this.onMouseEnter) document.addEventListener('mouseleave', this.onMouseLeave) document.querySelectorAll('a, button, [data-cursor]').forEach((el) => { el.addEventListener('mouseenter', this.onElementEnter) el.addEventListener('mouseleave', this.onElementLeave) }) this.tick() } private onMouseMove = (e: MouseEvent) => { this.state.mouse.x = e.clientX this.state.mouse.y = e.clientY this.dot.style.left = `${e.clientX}px` this.dot.style.top = `${e.clientY}px` } private tick = () => { this.state.follower.x += (this.state.mouse.x - this.state.follower.x) * this.LERP this.state.follower.y += (this.state.mouse.y - this.state.follower.y) * this.LERP this.ring.style.left = `${this.state.follower.x}px` this.ring.style.top = `${this.state.follower.y}px` this.rafId = requestAnimationFrame(this.tick) } private onElementEnter = (e: Event) => { const target = e.currentTarget as HTMLElement const cursorType = target.dataset.cursor || 'hover' this.setState('hover', cursorType) } private onElementLeave = () => { this.setState('default') } private setState(state: string, type?: string) { this.ring.className = `cursor-ring cursor-ring--${state}` if (type) this.ring.dataset.cursorType = type } destroy() { if (this.rafId) cancelAnimationFrame(this.rafId) document.removeEventListener('mousemove', this.onMouseMove) } } 

Cursor States and Their Implementation

Standard set: default, hover, active, text, drag, view. Switching via CSS classes and data attributes.

State CSS class Visual description
Default .cursor-ring--default Small ring without fill
Hover .cursor-ring--hover Enlarged circle with semi-transparent fill
Active .cursor-ring--active Shrunk circle
Text .cursor-ring--text Vertical bar
Drag .cursor-ring--drag Closed hand
View .cursor-ring--view Ring with text "VIEW"
.cursor-ring--hover { width: 52px; height: 52px; background: rgba(255, 255, 255, 0.1); border-color: transparent; transition: width 0.25s ease, height 0.25s ease, background 0.25s ease; } .cursor-ring--text { width: 2px; height: 28px; border-radius: 1px; background: #fff; border: none; } .cursor-ring[data-cursor-type="view"]::after { content: 'VIEW'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; color: #fff; } 

How to Implement a Custom Cursor Without Flickering?

Flickering occurs when dot and follower are rendered in different composite layers. Solution — both elements in the same stacking context and will-change: transform on each. We also use pointer-events: none so the cursor does not block clicks. In practice, this gives smoothness in 99% of cases — even at 60 fps. After this optimization, LCP does not increase, and subjective perception of speed improves by 20-30%.

Why Is It Important to Disable the Cursor on Touch Devices?

On touch input devices, a custom cursor is meaningless — it will hang at the last touch point. We disable it via the media query @media (hover: none) and (pointer: coarse), leaving the native browser cursor. This improves accessibility and saves battery — up to 5% charge during prolonged use.

@media (hover: hover) and (pointer: fine) { * { cursor: none; } .cursor-dot, .cursor-ring { display: block; } } @media (hover: none), (pointer: coarse) { .cursor-dot, .cursor-ring { display: none; } } 

Integration with React

In React projects, the cursor is implemented as a global component via Context or store. Important: mount only once, do not re-render on every mouse move. Here's an example with framer-motion useSpring, which reduces code by 3 times compared to manual RAF + lerp — that's 3 times better in development speed:

import { useEffect, useRef, useCallback } from 'react' import { useMotionValue, useSpring, motion } from 'framer-motion' export function CustomCursor() { const mouseX = useMotionValue(-100) const mouseY = useMotionValue(-100) const springConfig = { damping: 25, stiffness: 200, mass: 0.5 } const followerX = useSpring(mouseX, springConfig) const followerY = useSpring(mouseY, springConfig) useEffect(() => { const onMove = (e: MouseEvent) => { mouseX.set(e.clientX) mouseY.set(e.clientY) } window.addEventListener('mousemove', onMove) return () => window.removeEventListener('mousemove', onMove) }, []) const isTouchDevice = window.matchMedia('(hover: none)').matches if (isTouchDevice) return null return ( <> <motion.div className="cursor-dot" style={{ x: mouseX, y: mouseY, translateX: '-50%', translateY: '-50%' }} /> <motion.div className="cursor-ring" style={{ x: followerX, y: followerY, translateX: '-50%', translateY: '-50%' }} /> </> ) } 

Typical Pitfalls and Their Solutions

  • Flickering during fast movement — dot and ring in different layers. Solution: stacking context and will-change: transform.
  • Conflict with iframe — when the cursor enters an iframe, mousemove does not fire. Track mouseleave on document and hide the cursor.
  • CSS transition delay — if a transition is set on the ring without excluding left/top, the follower loses liveliness. Transitions only for scale/opacity/color.

Approach Comparison: RAF vs framer-motion

Parameter Pure RAF + lerp framer-motion useSpring
Code size ~50 lines of animation ~15 lines with handler
Performance High (0 extra dependencies) High (native RAF)
Flexibility Full control (custom easing) Limited to spring physics
React integration Requires ref + useEffect Built-in (useMotionValue)
Support Any browser Framer Motion (React)
Development time 3-4 hours 1 hour

In practice, framer-motion speeds up development by 3 times and provides smoothness at 60 fps. If the project is on Vue or vanilla JS — we use pure RAF.

What's Included in the Work

  • Source code of the cursor component (TypeScript + CSS)
  • API documentation and states
  • Integration instructions for React, Vue, or vanilla JS
  • Support for 14 days after delivery
  • Consultation on touch device adaptation

Work Plan and Timeline

Step Activity Duration
Design analysis Determine up to 10 cursor states 4–8 hours
Prototyping Create a test page with canvas or DOM 2–4 hours
Integration Connect the component to the framework 4–8 hours
Testing Check on 10+ device configurations 2–4 hours
Documentation Describe the API for state switching 2–4 hours

Basic implementation with dot + ring and hover states — from 1 day, starting at $500. With a set of 6 states, React adaptation, and full testing — 2–3 days, up to $2,000. Thanks to framer-motion, we cut development time by 3 times, providing significant budget savings.

Order a custom cursor turnkey — write to our engineers. We'll evaluate your project in 1 day and choose the optimal solution.