Getting started

Button

Card

Mouse tilt

3D Mouse Tilt Effect. Creates a 3D tilt effect that follows the mouse cursor.

card caption

Title

Subtitle

avatar

Kaida

Sep 6, 2024

Glare

You can use data-tilt-glare to add glare effect.

card caption

Title

Subtitle

avatar

Kaida

Sep 6, 2024

With target

You can use data-tilt-target for child tilt effect with container mouse hover.

Self prevent

You can use data-tilt-self-prevent to block tilt effect when hovering over the tilt target. When enabled, the element stops moving when you hover over it.

See our real usage from the "Launching soon" page:

Launching soon!

Generate custom design system that speaks both des and dev.
Sane defaults, framework-agnostic components,
and AI-powered customization.

Usage

To use this component copy our js code:

/**
 * 3D Mouse Tilt Effect
 * Creates a 3D tilt effect that follows the mouse cursor
 */
class MouseTilt3D {
  constructor(container, target_or_options, options = {}) {
    // Determine if second parameter is target element or options
    let target;
    
    if (target_or_options instanceof HTMLElement) {
      // If second parameter is HTMLElement, it's the target
      target = target_or_options;
      // Use provided options as third parameter
    } else {
      // If second parameter is not HTMLElement, it's options
      target = container;
      options = target_or_options || {};
    }
    
    // Save container and target elements
    this.container = container;
    this.target = target;

    // Merge default options with user-defined options
    this.options = {
      max_tilt: 15, // Maximum tilt angle in degrees
      perspective: 1000, // CSS perspective value
      easing: 'cubic-bezier(0.03, 0.98, 0.52, 0.99)', // Transition easing function
      scale: 1.05, // Scale factor on hover
      speed: 500, // Animation speed in milliseconds
      glare: false, // Enable glare effect
      glare_max_opacity: 0.7, // Maximum opacity for glare effect
      reset_on_leave: true, // Reset to initial state when mouse leaves
      self_prevent: false, // Block tilt effect when hovering over tilt target
      ...options,
    };

    // Disable glare if self_prevent is enabled (inconsistent to show glare when tilt is blocked)
    if (this.options.self_prevent) {
      this.options.glare = false;
    }

    // Internal state variables
    this.is_active = true;
    this.glare_element = null;
    this.animation_frame = null;
    this.is_over_target = false; // Track if mouse is over target element
    this.is_transform_reset = true; // Track if transform is in reset state

    // Store original styles to restore on destroy
    this.original_target_styles = {
      transform: target.style.transform,
      transition: target.style.transition,
      transform_style: target.style.transformStyle,
      backface_visibility: target.style.backfaceVisibility,
      will_change: target.style.willChange,
    };

    // Bind event handlers to maintain 'this' context
    this.handle_mouse_enter = this.handle_mouse_enter.bind(this);
    this.handle_mouse_move = this.handle_mouse_move.bind(this);
    this.handle_mouse_leave = this.handle_mouse_leave.bind(this);

    // Initialize the effect
    this.init();
  }

  /**
   * Initialize the tilt effect
   */
  init() {
    // Set up target styles for 3D effect
    this.setup_target();

    // Create glare element if enabled
    if (this.options.glare) {
      this.create_glare();
    }

    // Add event listeners to container
    this.add_event_listeners();
  }

  /**
   * Set up target element with necessary CSS styles
   */
  setup_target() {
    // Apply 3D transform styles to target element
    this.target.style.transformStyle = 'preserve-3d';
    this.target.style.backfaceVisibility = 'hidden';
    this.target.style.willChange = 'transform';
    this.target.style.transition = `transform ${this.options.speed}ms ${this.options.easing}`;
    
    // If target is different from container, ensure it has proper positioning
    if (this.target !== this.container) {
      // Ensure target has position relative or absolute for proper 3D context
      const computed_style = window.getComputedStyle(this.target);
      if (computed_style.position === 'static') {
        this.target.style.position = 'relative';
      }
    }
  }

  /**
   * Create glare effect element
   */
  create_glare() {
    this.glare_element = document.createElement('div');

    // Set glare element styles
    this.glare_element.style.position = 'absolute';
    this.glare_element.style.top = '0';
    this.glare_element.style.left = '0';
    this.glare_element.style.width = '100%';
    this.glare_element.style.height = '100%';
    this.glare_element.style.pointerEvents = 'none';
    this.glare_element.style.borderRadius = 'inherit';
    this.glare_element.style.opacity = '0';
    this.glare_element.style.transition = `opacity ${this.options.speed}ms ease`;
    this.glare_element.style.zIndex = '2';

    // Ensure container has relative positioning for absolute child
    const container_style = window.getComputedStyle(this.container);
    if (container_style.position === 'static') {
      this.container.style.position = 'relative';
    }

    // Add glare element to target (not container)
    this.target.appendChild(this.glare_element);
  }

  /**
   * Add mouse event listeners to container
   */
  add_event_listeners() {
    this.container.addEventListener('mouseenter', this.handle_mouse_enter);
    this.container.addEventListener('mousemove', this.handle_mouse_move);
    this.container.addEventListener('mouseleave', this.handle_mouse_leave);
  }

  /**
   * Handle mouse enter event
   */
  handle_mouse_enter() {
    if (!this.is_active) return;

    // Apply initial scale transform to target
    this.target.style.transform = `
      perspective(${this.options.perspective}px)
      scale3d(${this.options.scale}, ${this.options.scale}, ${this.options.scale})
    `;

    this.is_transform_reset = false;
    this.is_over_target = false;

    // Show glare effect if enabled
    if (this.glare_element) {
      this.glare_element.style.opacity = '1';
    }
  }

  /**
   * Handle mouse move event
   * Calculates tilt based on mouse position relative to container
   */
  handle_mouse_move(event) {
    if (!this.is_active) return;

    // Block tilt effect when hovering over tilt target
    if (this.options.self_prevent) {
      const target_rect = this.target.getBoundingClientRect();
      const mouse_x = event.clientX;
      const mouse_y = event.clientY;

      // Check if mouse is within target bounds
      const currently_over_target = (
        mouse_x >= target_rect.left &&
        mouse_x <= target_rect.right &&
        mouse_y >= target_rect.top &&
        mouse_y <= target_rect.bottom
      );

      // If mouse is over target element, reset transform and skip tilt
      if (currently_over_target) {
        this.reset_transform();
        this.is_over_target = true;
        return;
      }

      // If mouse just left the target element, reset transform
      if (this.is_over_target) {
        this.reset_transform();
        this.is_over_target = false;
      }
    }

    // Cancel any pending animation frame
    if (this.animation_frame) {
      cancelAnimationFrame(this.animation_frame);
    }

    // Use requestAnimationFrame for smooth animation
    this.animation_frame = requestAnimationFrame(() => {
      this.calculate_and_apply_tilt(event);
    });
  }

  /**
   * Reset target transform to initial state
   */
  reset_transform() {
    if (!this.options.reset_on_leave || this.is_transform_reset) return;

    this.target.style.transform = `
      perspective(${this.options.perspective}px)
      rotateX(0deg)
      rotateY(0deg)
      scale3d(1, 1, 1)
    `;

    this.is_transform_reset = true;
  }

  /**
   * Calculate tilt angles and apply transform to target
   */
  calculate_and_apply_tilt(event) {
    this.is_transform_reset = false;

    // Use container bounds for mouse tracking
    const container_rect = this.container.getBoundingClientRect();

    // Calculate container center
    const center_x = container_rect.left + container_rect.width / 2;
    const center_y = container_rect.top + container_rect.height / 2;

    // Calculate mouse position relative to center (-1 to 1)
    const mouse_x = event.clientX - center_x;
    const mouse_y = event.clientY - center_y;

    const percentage_x = mouse_x / (container_rect.width / 2);
    const percentage_y = mouse_y / (container_rect.height / 2);

    // Calculate tilt angles based on mouse position
    const tilt_x = (this.options.max_tilt * percentage_y * -1).toFixed(2);
    const tilt_y = (this.options.max_tilt * percentage_x).toFixed(2);

    // Apply the transform to target element
    this.target.style.transform = `
      perspective(${this.options.perspective}px)
      rotateX(${tilt_x}deg)
      rotateY(${tilt_y}deg)
      scale3d(${this.options.scale}, ${this.options.scale}, ${this.options.scale})
    `;

    // Update glare position if enabled
    this.update_glare_position(percentage_x, percentage_y);
  }

  /**
   * Update glare position based on mouse position
   */
  update_glare_position(percentage_x, percentage_y) {
    if (!this.glare_element) return;

    // Calculate glare gradient angle based on mouse position
    const glare_angle = percentage_x * 45;

    // Update glare gradient
    this.glare_element.style.background = `linear-gradient(
      ${glare_angle}deg,
      rgba(255, 255, 255, 0) 0%,
      rgba(255, 255, 255, ${this.options.glare_max_opacity}) 100%
    )`;
  }

  /**
   * Handle mouse leave event
   */
  handle_mouse_leave() {
    if (!this.is_active || !this.options.reset_on_leave) return;

    // Reset transform to initial state
    this.reset_transform();

    // Hide glare effect if enabled
    if (this.glare_element) {
      this.glare_element.style.opacity = '0';
    }

    // Reset target tracking state
    this.is_over_target = false;
  }

  /**
   * Update effect options
   * @param {Object} new_options - New options to merge with current options
   */
  update(new_options) {
    this.options = { ...this.options, ...new_options };

    // Update transition if speed changed
    this.target.style.transition = 
      `transform ${this.options.speed}ms ${this.options.easing}`;
  }

  /**
   * Enable the tilt effect
   */
  enable() {
    this.is_active = true;
  }

  /**
   * Disable the tilt effect
   */
  disable() {
    this.is_active = false;
    this.target.style.transform = '';
    
    if (this.glare_element) {
      this.glare_element.style.opacity = '0';
    }
  }

  /**
   * Remove event listeners and restore original styles
   */
  destroy() {
    // Remove event listeners from container
    this.container.removeEventListener('mouseenter', this.handle_mouse_enter);
    this.container.removeEventListener('mousemove', this.handle_mouse_move);
    this.container.removeEventListener('mouseleave', this.handle_mouse_leave);

    // Cancel any pending animation frame
    if (this.animation_frame) {
      cancelAnimationFrame(this.animation_frame);
    }

    // Restore original target styles
    this.target.style.transform = this.original_target_styles.transform;
    this.target.style.transition = this.original_target_styles.transition;
    this.target.style.transformStyle = this.original_target_styles.transform_style;
    this.target.style.backfaceVisibility = this.original_target_styles.backface_visibility;
    this.target.style.willChange = this.original_target_styles.will_change;

    // Remove glare element if exists
    if (this.glare_element && this.glare_element.parentNode) {
      this.glare_element.parentNode.removeChild(this.glare_element);
    }

    // Clear references
    this.container = null;
    this.target = null;
    this.glare_element = null;
  }
}

/**
 * Factory function to create a new MouseTilt3D instance
 * Supports two usage patterns:
 * 1. add_mouse_tilt_effect(container, options) - effect applied to container
 * 2. add_mouse_tilt_effect(container, target, options) - effect applied to target
 * 
 * @param {HTMLElement} container - The element to track mouse movement
 * @param {HTMLElement|Object} target_or_options - Target element or options object
 * @param {Object} options - Configuration options for the effect
 * @returns {MouseTilt3D} A new instance of MouseTilt3D
 */
const add_mouse_tilt_effect = (container, target_or_options, options = {}) => {
  // Determine if second parameter is target element or options
  if (target_or_options instanceof HTMLElement) {
    // Three parameters: container, target, options
    return new MouseTilt3D(container, target_or_options, options);
  } else {
    // Two parameters: container, options (target = container)
    return new MouseTilt3D(container, target_or_options || {});
  }
};

/**
 * Initialize all tilt effects on the page
 * Supports data attributes for both container and target
 * 
 * @param {Object} default_options - Default options for all tilt effects
 */
const init_tilt_effects = (default_options = {}) => {
  // Find all elements with data-tilt attribute (these are containers)
  document.querySelectorAll('[data-tilt]').forEach((container) => {
    // Check if effect is already initialized on this container
    if (container._tilt_effect) {
      return; // Skip already initialized elements
    }

    // Parse options from data attributes
    const data_options = {};

    // Get max tilt from data attribute
    if (container.dataset.tiltMax) {
      data_options.max_tilt = parseFloat(container.dataset.tiltMax);
    }

    // Get perspective from data attribute
    if (container.dataset.tiltPerspective) {
      data_options.perspective = parseFloat(container.dataset.tiltPerspective);
    }

    // Get scale from data attribute
    if (container.dataset.tiltScale) {
      data_options.scale = parseFloat(container.dataset.tiltScale);
    }

    // Check if glare is enabled
    if (container.dataset.tiltGlare) {
      data_options.glare = container.dataset.tiltGlare === 'true';
    }

    // Check if reset on leave is disabled
    if (container.dataset.tiltResetOnLeave) {
      data_options.reset_on_leave = container.dataset.tiltResetOnLeave === 'true';
    }

    // Check if self-prevent is enabled (block tilt when hovering over tilt target)
    if (container.dataset.tiltSelfPrevent) {
      data_options.self_prevent = container.dataset.tiltSelfPrevent === 'true';
    }

    // Check if target element is specified
    let target_element = container;
    if (container.dataset.tiltTarget) {
      const target_selector = container.dataset.tiltTarget;
      target_element = container.querySelector(target_selector) || 
                       document.querySelector(target_selector);
      
      if (!target_element) {
        console.warn(`MouseTilt3D: Target element "${target_selector}" not found`);
        target_element = container;
      }
    }

    // Merge all options: data attributes override defaults
    const options = { ...default_options, ...data_options };

    // Create new tilt effect instance using factory function
    const tilt_effect = add_mouse_tilt_effect(container, target_element, options);

    // Store reference to tilt effect on container for later access
    container._tilt_effect = tilt_effect;
  });
};

/**
 * Remove tilt effect from specific container
 * @param {HTMLElement} container - Container element to remove tilt effect from
 */
const remove_mouse_tilt_effect = (container) => {
  if (container._tilt_effect) {
    container._tilt_effect.destroy();
    delete container._tilt_effect;
  }
};

/**
 * Remove all tilt effects from the page
 */
const destroy_all_tilt_effects = () => {
  document.querySelectorAll('[data-tilt]').forEach((container) => {
    remove_mouse_tilt_effect(container);
  });
};

// Make functions available globally
window.add_mouse_tilt_effect = add_mouse_tilt_effect;
window.init_tilt_effects = init_tilt_effects;
window.remove_mouse_tilt_effect = remove_mouse_tilt_effect;
window.destroy_all_tilt_effects = destroy_all_tilt_effects;

// Export for module usage (if supported)
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    MouseTilt3D,
    add_mouse_tilt_effect,
    init_tilt_effects,
    remove_mouse_tilt_effect,
    destroy_all_tilt_effects,
  };
}

Then init effect:

init_tilt_effects();

Or initialize with custom params:

init_tilt_effects({
  max_tilt: 15,
  glare: false,
  scale: 1.05,
});

Auto init

Insert in the end of JS (mouse-tilt.js) file this:

document.addEventListener("DOMContentLoaded", () => init_tilt_effects());

Then connect:

<script src="mouse-tilt.js"></script>

Params

Available parameters for use in init_tilt_effects or add_mouse_tilt_effect:

init_tilt_effects({
  max_tilt: 15, // Maximum tilt angle in degrees
  perspective: 1000, // CSS perspective value
  easing: 'cubic-bezier(0.03, 0.98, 0.52, 0.99)', // Transition easing function
  scale: 1.05, // Scale factor on hover
  speed: 500, // Animation speed in milliseconds
  glare: false, // Enable glare effect
  glare_max_opacity: 0.7, // Maximum opacity for glare effect
  reset_on_leave: true, // Reset to initial state when mouse leaves
  self_prevent: false, // Block tilt effect when hovering over tilt target
});

// Or with window method
const tilt2 = add_mouse_tilt_effect(parent, child, {
  max_tilt: 25,
  perspective: 1200,
  scale: 1.05,
  glare: false,
});

// Or with module
const tilt_effect = new MouseTilt3D(container, {
  max_tilt: 20,
  perspective: 1500,
  scale: 1.1,
  glare: true,
  glare_max_opacity: 0.5,
  speed: 300,
});

You can control the effect using the returned instance:

tilt_effect.disable(); // Temporarily disable the effect
tilt_effect.enable();  // Re-enable the effect
tilt_effect.update({ max_tilt: 30 }); // Update options
tilt_effect.destroy(); // Completely remove the effect

These attributes override the global options passed to init_tilt_effects() for that specific tilt effect.

Data attributes

You can also configure individual tilt effects via data attributes on the container:

  • data-tilt-max – maximum tilt angle in degrees (same as max_tilt).
  • data-tilt-perspective – CSS perspective value (same as perspective).
  • data-tilt-scale – scale factor on hover (same as scale).
  • data-tilt-glare – enable glare effect (boolean, "true" or "false").
  • data-tilt-reset-on-leave – reset to initial state when mouse leaves (boolean).
  • data-tilt-target – selector for target element to apply tilt effect.
  • data-tilt-self-prevent – block tilt effect when hovering over tilt target (boolean).