Dynamic Cursors in Web Design: Tutorial + Code Examples

The cursor is the most direct connection between user and interface. Yet most websites ignore it completely.

Custom cursors aren’t new. Flash sites in the 2000s used them extensively (often poorly). But modern CSS and JavaScript make dynamic cursors practical, performant, and elegant.

Done well, custom cursors enhance the experience. They provide context, create immersion, and add personality. A cursor that changes based on what you’re hovering over tells you what will happen before you click. A cursor that follows with a trailing effect adds playfulness without obstructing.

Done poorly, custom cursors frustrate users by being slow, confusing, or inaccessible.

This tutorial teaches you how to create dynamic cursors that enhance rather than annoy. We’ll cover basic CSS cursors, custom cursor images, JavaScript-powered cursors, and advanced effects like magnetic cursors and cursor trails.

TL;DR: Dynamic Cursor Essentials

  • CSS cursor property – Simple built-in cursors and custom images
  • JavaScript for advanced effects – Follow mouse position, create trails
  • Keep it performant – Use transform, avoid layout changes
  • Provide context – Cursor should indicate what action will happen
  • Don’t obstruct content – Cursor shouldn’t hide what user needs to see
  • Mobile doesn’t have cursors – Design for touch simultaneously
  • Accessibility matters – Some users can’t see cursor well
  • Test across browsers – Cursor rendering varies

Understanding the Cursor Property

CSS provides the cursor property to change cursor appearance. This is the foundation for all cursor customization.

Built-In Cursor Values

CSS includes many cursor values for common contexts:

/* Basic cursors */
.default { cursor: default; }        /* Standard arrow */
.pointer { cursor: pointer; }        /* Hand pointing (for links) */
.text { cursor: text; }              /* I-beam for text selection */
.move { cursor: move; }              /* Four-way arrows for dragging */
.not-allowed { cursor: not-allowed; } /* Circle with slash */

/* Resize cursors */
.resize-n { cursor: n-resize; }      /* Resize north */
.resize-s { cursor: s-resize; }      /* Resize south */
.resize-e { cursor: e-resize; }      /* Resize east */
.resize-w { cursor: w-resize; }      /* Resize west */
.resize-ne { cursor: ne-resize; }    /* Resize northeast */
.resize-nw { cursor: nw-resize; }    /* Resize northwest */
.resize-se { cursor: se-resize; }    /* Resize southeast */
.resize-sw { cursor: sw-resize; }    /* Resize southwest */

/* Action cursors */
.help { cursor: help; }              /* Question mark */
.wait { cursor: wait; }              /* Loading spinner */
.progress { cursor: progress; }      /* Arrow with spinner */
.crosshair { cursor: crosshair; }    /* Crosshair for precision */
.grab { cursor: grab; }              /* Open hand */
.grabbing { cursor: grabbing; }      /* Closed hand */

/* Form cursors */
.cell { cursor: cell; }              /* Table cell selection */
.copy { cursor: copy; }              /* Copy action */
.alias { cursor: alias; }            /* Shortcut creation */
.zoom-in { cursor: zoom-in; }        /* Magnifier plus */
.zoom-out { cursor: zoom-out; }      /* Magnifier minus */

Using built-in cursors is fast, accessible, and familiar. Always start here before creating custom cursors.

Custom Cursor Images

You can use custom images as cursors:

.custom-cursor {
  cursor: url('cursor.png'), auto;
  /* Format: url(image), fallback */
}

/* With hotspot positioning */
.custom-cursor-positioned {
  cursor: url('cursor.png') 10 10, auto;
  /* Numbers are x and y offset from top-left */
}

Important limitations:

  • Maximum size: 32×32 pixels in most browsers (128×128 in some)
  • File formats: PNG, GIF, SVG (support varies)
  • Always provide fallback cursor
  • Cursor doesn’t scale for high-DPI displays automatically

SVG Cursors for Scalability

SVG cursors scale perfectly on retina displays:

.svg-cursor {
  cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><circle cx="16" cy="16" r="10" fill="red"/></svg>') 16 16, auto;
}

The data URI embeds the SVG directly in CSS. The numbers (16 16) position the hotspot at the center.

Multiple Cursor Fallbacks

Provide multiple cursor options for broader compatibility:

.cursor-with-fallbacks {
  cursor: url('cursor.svg'), url('cursor.png'), pointer;
  /* Tries SVG first, then PNG, then built-in pointer */
}

Browsers use the first format they support.

Simple Custom Cursor Implementation

Let’s build a basic custom cursor that replaces the default arrow.

HTML Structure

<div class="custom-cursor"></div>
<div class="content">
  <!-- Your page content -->
  <button class="hover-target">Hover me</button>
</div>

CSS Styling

/* Hide default cursor */
body {
  cursor: none;
}

/* Custom cursor element */
.custom-cursor {
  width: 20px;
  height: 20px;
  border: 2px solid #000;
  border-radius: 50%;
  position: fixed;
  pointer-events: none; /* Critical: allows clicking through cursor */
  z-index: 9999;
  transform: translate(-50%, -50%);
  transition: width 0.2s, height 0.2s;
}

/* Hover state */
.custom-cursor.hovering {
  width: 40px;
  height: 40px;
  background: rgba(0, 0, 0, 0.1);
}

pointer-events: none is critical. Without it, the cursor element intercepts clicks meant for elements beneath it.

JavaScript Tracking

const cursor = document.querySelector('.custom-cursor');
const hoverTargets = document.querySelectorAll('.hover-target');

// Update cursor position
document.addEventListener('mousemove', (e) => {
  cursor.style.left = e.clientX + 'px';
  cursor.style.top = e.clientY + 'px';
});

// Add hover effects
hoverTargets.forEach(target => {
  target.addEventListener('mouseenter', () => {
    cursor.classList.add('hovering');
  });
  
  target.addEventListener('mouseleave', () => {
    cursor.classList.remove('hovering');
  });
});

// Hide cursor when leaving window
document.addEventListener('mouseleave', () => {
  cursor.style.opacity = '0';
});

document.addEventListener('mouseenter', () => {
  cursor.style.opacity = '1';
});

This creates a simple custom cursor that grows when hovering over interactive elements.

Smooth Cursor Following with Easing

The basic implementation updates cursor position instantly. Adding easing creates smoother, more natural movement.

Lerp (Linear Interpolation)

Lerp smoothly interpolates between current position and target position:

const cursor = document.querySelector('.custom-cursor');

let mouseX = 0;
let mouseY = 0;
let cursorX = 0;
let cursorY = 0;
const speed = 0.15; // Lower = smoother but slower

document.addEventListener('mousemove', (e) => {
  mouseX = e.clientX;
  mouseY = e.clientY;
});

function animate() {
  // Calculate distance
  const distX = mouseX - cursorX;
  const distY = mouseY - cursorY;
  
  // Move cursor fraction of distance (easing effect)
  cursorX += distX * speed;
  cursorY += distY * speed;
  
  // Update cursor position
  cursor.style.left = cursorX + 'px';
  cursor.style.top = cursorY + 'px';
  
  requestAnimationFrame(animate);
}

animate();

This creates smooth following motion. Adjust speed value for different feels (0.1 = very smooth, 0.3 = more responsive).

Performance Optimization

Use CSS transforms instead of left/top for better performance:

function animate() {
  const distX = mouseX - cursorX;
  const distY = mouseY - cursorY;
  
  cursorX += distX * speed;
  cursorY += distY * speed;
  
  // Use transform for hardware acceleration
  cursor.style.transform = `translate(${cursorX}px, ${cursorY}px)`;
  
  requestAnimationFrame(animate);
}

Transforms are hardware-accelerated and don’t trigger layout recalculation.

Cursor Trail Effect

Create trailing circles that follow the cursor, creating a comet-like effect.

HTML Structure

<div class="cursor-trail"></div>

JavaScript Implementation

const trailContainer = document.querySelector('.cursor-trail');
const trailCount = 10;
const trails = [];

// Create trail elements
for (let i = 0; i < trailCount; i++) {
  const trail = document.createElement('div');
  trail.className = 'trail-dot';
  trail.style.cssText = `
    position: fixed;
    width: 10px;
    height: 10px;
    background: rgba(255, 0, 0, ${1 - i / trailCount});
    border-radius: 50%;
    pointer-events: none;
    transform: translate(-50%, -50%);
  `;
  trailContainer.appendChild(trail);
  trails.push({ element: trail, x: 0, y: 0 });
}

let mouseX = 0;
let mouseY = 0;

document.addEventListener('mousemove', (e) => {
  mouseX = e.clientX;
  mouseY = e.clientY;
});

function animateTrail() {
  let prevX = mouseX;
  let prevY = mouseY;
  
  trails.forEach((trail, index) => {
    // Each trail follows the one before it
    const distX = prevX - trail.x;
    const distY = prevY - trail.y;
    
    trail.x += distX * 0.3;
    trail.y += distY * 0.3;
    
    trail.element.style.transform = 
      `translate(${trail.x}px, ${trail.y}px) scale(${1 - index * 0.1})`;
    
    prevX = trail.x;
    prevY = trail.y;
  });
  
  requestAnimationFrame(animateTrail);
}

animateTrail();

Each dot follows the one before it, creating a snake-like trail.

Magnetic Cursor Effect

Cursor “snaps” toward interactive elements when nearby, creating satisfying magnetic attraction.

HTML

<button class="magnetic-button">Magnetic Button</button>
<div class="custom-cursor"></div>

CSS

.magnetic-button {
  padding: 20px 40px;
  font-size: 18px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: none;
  transition: transform 0.3s ease-out;
}

.custom-cursor {
  width: 20px;
  height: 20px;
  border: 2px solid #007bff;
  border-radius: 50%;
  position: fixed;
  pointer-events: none;
  z-index: 9999;
  transition: width 0.3s, height 0.3s, border-color 0.3s;
}

.custom-cursor.attracted {
  width: 60px;
  height: 60px;
  border-color: rgba(0, 123, 255, 0.3);
}

JavaScript

const cursor = document.querySelector('.custom-cursor');
const magneticElements = document.querySelectorAll('.magnetic-button');

let mouseX = 0;
let mouseY = 0;
let cursorX = 0;
let cursorY = 0;

document.addEventListener('mousemove', (e) => {
  mouseX = e.clientX;
  mouseY = e.clientY;
});

function checkMagnetic() {
  let attracted = false;
  
  magneticElements.forEach(element => {
    const rect = element.getBoundingClientRect();
    const elementCenterX = rect.left + rect.width / 2;
    const elementCenterY = rect.top + rect.height / 2;
    
    // Calculate distance from cursor to element center
    const distX = elementCenterX - mouseX;
    const distY = elementCenterY - mouseY;
    const distance = Math.sqrt(distX * distX + distY * distY);
    
    const magneticRadius = 100; // Attraction range
    
    if (distance < magneticRadius) {
      attracted = true;
      
      // Pull cursor toward element
      const pullStrength = 1 - (distance / magneticRadius);
      cursorX = mouseX + distX * pullStrength * 0.3;
      cursorY = mouseY + distY * pullStrength * 0.3;
      
      // Pull element slightly toward cursor
      const elementPullX = -distX * pullStrength * 0.1;
      const elementPullY = -distY * pullStrength * 0.1;
      element.style.transform = 
        `translate(${elementPullX}px, ${elementPullY}px)`;
      
      cursor.classList.add('attracted');
    } else {
      element.style.transform = 'translate(0, 0)';
    }
  });
  
  if (!attracted) {
    cursorX += (mouseX - cursorX) * 0.15;
    cursorY += (mouseY - cursorY) * 0.15;
    cursor.classList.remove('attracted');
  }
  
  cursor.style.transform = `translate(${cursorX}px, ${cursorY}px)`;
  
  requestAnimationFrame(checkMagnetic);
}

checkMagnetic();

Cursor and button both move toward each other when close, creating magnetic attraction effect.

Context-Aware Cursors

Cursor changes based on what element it’s hovering over, providing visual feedback about available actions.

Implementation

const cursor = document.querySelector('.custom-cursor');

// Different cursor styles for different contexts
const cursorStyles = {
  default: {
    width: '20px',
    height: '20px',
    background: 'transparent',
    border: '2px solid #000'
  },
  link: {
    width: '40px',
    height: '40px',
    background: 'rgba(0, 123, 255, 0.2)',
    border: '2px solid #007bff'
  },
  button: {
    width: '50px',
    height: '50px',
    background: 'rgba(0, 255, 0, 0.2)',
    border: '2px solid #00ff00'
  },
  text: {
    width: '2px',
    height: '20px',
    background: '#000',
    border: 'none'
  }
};

function applyCursorStyle(style) {
  Object.assign(cursor.style, cursorStyles[style]);
}

// Apply styles based on element type
document.addEventListener('mouseover', (e) => {
  const target = e.target;
  
  if (target.tagName === 'A') {
    applyCursorStyle('link');
    cursor.innerHTML = '→'; // Add arrow
  } else if (target.tagName === 'BUTTON') {
    applyCursorStyle('button');
    cursor.innerHTML = '+';
  } else if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
    applyCursorStyle('text');
    cursor.innerHTML = '';
  } else {
    applyCursorStyle('default');
    cursor.innerHTML = '';
  }
});

Cursor visually changes to indicate what action is available.

Text Selection Cursor

Custom cursor for text selection that shows you’re in text mode.

.text-cursor {
  width: 2px;
  height: 24px;
  background: #000;
  position: fixed;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s;
}

.text-cursor.active {
  opacity: 1;
  animation: blink 1s infinite;
}

@keyframes blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}
const textCursor = document.querySelector('.text-cursor');

document.addEventListener('mouseover', (e) => {
  const target = e.target;
  
  if (target.tagName === 'P' || 
      target.tagName === 'INPUT' || 
      target.tagName === 'TEXTAREA') {
    textCursor.classList.add('active');
  } else {
    textCursor.classList.remove('active');
  }
});

Creates blinking text cursor effect over text areas.

Cursor with Text Label

Display text next to cursor showing what action is available.

<div class="cursor-wrapper">
  <div class="cursor-dot"></div>
  <div class="cursor-label">Click to view</div>
</div>
.cursor-wrapper {
  position: fixed;
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
}

.cursor-dot {
  width: 10px;
  height: 10px;
  background: #000;
  border-radius: 50%;
}

.cursor-label {
  position: absolute;
  left: 20px;
  top: -5px;
  background: #000;
  color: #fff;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  white-space: nowrap;
  opacity: 0;
  transform: translateX(-10px);
  transition: opacity 0.2s, transform 0.2s;
}

.cursor-wrapper.show-label .cursor-label {
  opacity: 1;
  transform: translateX(0);
}
const cursorWrapper = document.querySelector('.cursor-wrapper');
const cursorLabel = document.querySelector('.cursor-label');

document.addEventListener('mousemove', (e) => {
  cursorWrapper.style.left = e.clientX + 'px';
  cursorWrapper.style.top = e.clientY + 'px';
});

document.addEventListener('mouseover', (e) => {
  const target = e.target;
  
  if (target.hasAttribute('data-cursor-text')) {
    cursorLabel.textContent = target.getAttribute('data-cursor-text');
    cursorWrapper.classList.add('show-label');
  } else {
    cursorWrapper.classList.remove('show-label');
  }
});

Use with: <img src="..." data-cursor-text="Click to expand">

Image Preview Cursor

Show image preview next to cursor when hovering over image links.

<div class="image-preview-cursor">
  <img src="" alt="">
</div>
.image-preview-cursor {
  position: fixed;
  width: 200px;
  height: 150px;
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  transform: translate(20px, 20px) scale(0.8);
  transition: opacity 0.3s, transform 0.3s;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}

.image-preview-cursor.active {
  opacity: 1;
  transform: translate(20px, 20px) scale(1);
}

.image-preview-cursor img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
const previewCursor = document.querySelector('.image-preview-cursor');
const previewImage = previewCursor.querySelector('img');

document.addEventListener('mousemove', (e) => {
  previewCursor.style.left = e.clientX + 'px';
  previewCursor.style.top = e.clientY + 'px';
});

document.addEventListener('mouseover', (e) => {
  const target = e.target;
  
  if (target.tagName === 'A' && target.hasAttribute('data-preview')) {
    const imageUrl = target.getAttribute('data-preview');
    previewImage.src = imageUrl;
    previewCursor.classList.add('active');
  }
});

document.addEventListener('mouseout', (e) => {
  if (e.target.tagName === 'A') {
    previewCursor.classList.remove('active');
  }
});

Cursor Particles

Particles emit from cursor on click, creating playful interaction.

function createParticle(x, y) {
  const particle = document.createElement('div');
  particle.className = 'cursor-particle';
  
  particle.style.cssText = `
    position: fixed;
    width: 8px;
    height: 8px;
    background: hsl(${Math.random() * 360}, 70%, 60%);
    border-radius: 50%;
    pointer-events: none;
    z-index: 9999;
    left: ${x}px;
    top: ${y}px;
  `;
  
  document.body.appendChild(particle);
  
  // Random direction
  const angle = Math.random() * Math.PI * 2;
  const velocity = 2 + Math.random() * 4;
  let vx = Math.cos(angle) * velocity;
  let vy = Math.sin(angle) * velocity;
  let life = 1;
  
  function animate() {
    // Apply gravity
    vy += 0.2;
    
    // Update position
    x += vx;
    y += vy;
    
    // Fade out
    life -= 0.02;
    
    if (life > 0) {
      particle.style.left = x + 'px';
      particle.style.top = y + 'px';
      particle.style.opacity = life;
      requestAnimationFrame(animate);
    } else {
      particle.remove();
    }
  }
  
  animate();
}

document.addEventListener('click', (e) => {
  // Create multiple particles on click
  for (let i = 0; i < 8; i++) {
    createParticle(e.clientX, e.clientY);
  }
});

Creates burst of colorful particles on every click.

Accessibility Considerations

Custom cursors can create accessibility problems. Address them proactively.

Provide Fallbacks

Never hide the default cursor without providing functional replacement:

/* Bad: Just hiding cursor */
body {
  cursor: none;
}

/* Good: Hide default, show custom */
body {
  cursor: none;
}

.custom-cursor {
  /* Custom cursor implementation */
}

/* Fallback for non-JavaScript */
.no-js body {
  cursor: auto !important;
}

Respect User Preferences

Some users need high contrast or large cursors:

// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (prefersReducedMotion) {
  // Disable animations, use simple cursor
  document.body.classList.add('reduced-motion');
}
.reduced-motion .custom-cursor {
  transition: none;
  animation: none;
}

.reduced-motion body {
  cursor: auto; /* Use default cursor */
}

Keyboard Navigation

Custom cursors don’t help keyboard users. Ensure all functionality works without mouse:

// Ensure focus states are visible
document.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    document.body.classList.add('keyboard-nav');
  }
});

document.addEventListener('mousedown', () => {
  document.body.classList.remove('keyboard-nav');
});
.keyboard-nav *:focus {
  outline: 3px solid #007bff;
  outline-offset: 2px;
}

High Contrast Mode

Windows High Contrast Mode ignores custom cursors. Accept this gracefully:

@media (prefers-contrast: high) {
  .custom-cursor {
    display: none;
  }
  
  body {
    cursor: auto !important;
  }
}

Screen Reader Considerations

Custom cursors are visual only. Ensure screen reader users get equivalent information:

<!-- Bad: Only visual cursor change -->
<button class="magnetic-button">Submit</button>

<!-- Good: Accessible label -->
<button class="magnetic-button" aria-label="Submit form (click or press Enter)">
  Submit
</button>

Performance Optimization

Custom cursors can impact performance if implemented poorly.

Use Transform, Not Position

// Bad: Triggers layout recalculation
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';

// Good: Hardware accelerated
cursor.style.transform = `translate(${x}px, ${y}px)`;

Transforms are GPU-accelerated and much faster.

Throttle Mouse Events

Mouse moves fire very frequently. Throttle them if needed:

let ticking = false;

document.addEventListener('mousemove', (e) => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateCursor(e.clientX, e.clientY);
      ticking = false;
    });
    ticking = true;
  }
});

This ensures cursor updates only happen during animation frames.

Reduce DOM Manipulation

Creating and removing elements is expensive:

// Bad: Creating new elements constantly
function createTrail() {
  const trail = document.createElement('div');
  // ...
  setTimeout(() => trail.remove(), 1000);
}

// Good: Reuse element pool
const trailPool = [];
for (let i = 0; i < 10; i++) {
  const trail = document.createElement('div');
  trailPool.push(trail);
  document.body.appendChild(trail);
}

function useTrail(index) {
  const trail = trailPool[index];
  // Update trail position and opacity
}

Reusing elements is much faster than creating new ones.

Disable on Mobile

Mobile devices don’t have mouse cursors. Disable custom cursor code entirely:

const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

if (!isMobile) {
  // Initialize custom cursor
  initCustomCursor();
}

When to Use Custom Cursors

Not every site needs custom cursors. Use them strategically.

Good Use Cases

Creative portfolios: Designers, artists, and agencies showcasing creativity.

Interactive experiences: Games, interactive stories, immersive brand experiences.

Product showcases: Highlight products with cursor effects that enhance rather than distract.

Event sites: Temporary sites for events, launches, or campaigns where novelty adds value.

Bad Use Cases

E-commerce sites: Don’t interfere with purchase flow. Users need familiarity.

Corporate/enterprise sites: Professional contexts expect standard interactions.

Content-heavy sites: Blogs, news, documentation. Focus on reading, not cursor effects.

Accessibility-critical applications: Healthcare, government, education. Prioritize access over aesthetics.

The Balance

Even creative sites shouldn’t let custom cursors obstruct usability. Ask:

  • Does it add value or just look cool?
  • Can users still complete tasks easily?
  • Does it work on all target devices?
  • Is it accessible to users with disabilities?

If answers are no, reconsider.

Testing Custom Cursors

Test thoroughly before deploying custom cursors.

Cross-Browser Testing

Cursor rendering varies across browsers:

  • Chrome/Edge: Generally consistent
  • Firefox: May handle transforms differently
  • Safari: Sometimes has lag with complex animations

Test on all major browsers.

Performance Testing

Use browser dev tools to monitor:

Frame rate: Should maintain 60fps during cursor movement CPU usage: Should be minimal (under 10%) Memory: No memory leaks from creating elements

If performance suffers, simplify implementation.

Device Testing

Test on:

  • High-end desktop (should be silky smooth)
  • Mid-range laptop (should still work well)
  • Low-end device (might need to disable effects)

Consider progressive enhancement based on device capability.

User Testing

Watch real users interact with custom cursor:

  • Do they notice it positively or negatively?
  • Does it help or hinder task completion?
  • Do they find it delightful or annoying?

User feedback trumps personal preference.

Conclusion

Custom cursors can enhance interfaces when done thoughtfully. They provide context, create immersion, and add personality. But they must never obstruct usability or accessibility.

Key takeaways:

  • Start with CSS cursor property for simple customization
  • JavaScript enables advanced effects like trails and magnetic attraction
  • Use transform for positioning, never left/top
  • Always provide fallback cursors
  • Respect reduced motion and high contrast preferences
  • Mobile doesn’t have cursors, design accordingly
  • Test performance across devices and browsers
  • Use custom cursors strategically, not universally
  • Accessibility must never be sacrificed for aesthetics
  • Test with real users to validate effectiveness

The action you should take today: If you’re considering custom cursors, start simple. Try a basic CSS cursor change on hover states. Gauge user reaction before implementing complex JavaScript cursors.

Custom cursors are details that can delight or frustrate. Execute them well, and users smile. Execute them poorly, and users leave.

Ready to explore more dynamic design elements? Check out our guide on Bento Box Design: Modular Layouts for Modern Websites, where we cover grid-based layouts that adapt beautifully across screen sizes.

Quick Reference: Cursor Implementation Patterns

Basic Custom Cursor:

body { cursor: none; }
.cursor {
  position: fixed;
  pointer-events: none;
  transform: translate(-50%, -50%);
}

Smooth Following:

cursorX += (mouseX - cursorX) * 0.15;
cursor.style.transform = `translate(${cursorX}px, ${cursorY}px)`;

Magnetic Effect:

const distance = Math.sqrt(distX * distX + distY * distY);
const pull = 1 - (distance / radius);

Performance:

requestAnimationFrame(() => updateCursor());
// Use transform, not left/top

Frequently Asked Questions

Do custom cursors work on mobile? No. Mobile devices use touch, not mouse cursors. Always design for touch simultaneously and disable custom cursors on mobile.

Will custom cursors hurt SEO? No direct SEO impact. Indirectly, if they hurt user experience and increase bounce rates, that could affect rankings.

How do I make custom cursors accessible? Respect prefers-reduced-motion, provide fallbacks, ensure keyboard navigation works, never rely on cursor alone to convey information.

What’s the performance cost? Minimal if implemented well using transforms and requestAnimationFrame. Can be significant if using inefficient code or excessive DOM manipulation.

Can I use custom cursors in production? Yes, but consider your audience and use case carefully. Creative sites benefit more than functional apps.

How do I disable custom cursor for specific users? Detect prefers-reduced-motion, high-contrast mode, or provide user setting to disable custom cursors.

References & Further Reading

  • MDN: CSS cursor property
  • “Designing with Cursors” – Various UX articles
  • Codrops: Cursor Effects
  • GSAP Animation Documentation
  • Web Performance Working Group
  • WCAG 2.2 Guidelines
  • “Interaction Design” – Sharp, Rogers, Preece