Cursors✨ Premium

Cursor Morph

A custom cursor that changes shape based on what it hovers: a diamond over a link, a blinking bar over a text input, a circle by default. Each state is driven by a data-morph attribute read on every event; position follows with a 15% lerp.

JSAdaptive

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →

3 usage examples

Sign-up or contact form — mixed field types — Cursor Morph example 1

① Sign-up or contact form — mixed field types

WhenA form mixes submit buttons, text inputs and legal links on the same surface. The cursor changes shape for each type of target.
WhyThe morph encodes the available interaction before the click: a blinking bar over an input, a diamond over a legal link. The zone becomes readable from the cursor alone, with no extra label.
SettingsApply data-morph="input" to every <input> and <textarea>, data-morph="link" to legal anchor links, data-morph="button" to the submit button. Raise the lerp to 0.20.
Navigation bar — links and submenu triggers — Cursor Morph example 2

② Navigation bar — links and submenu triggers

WhenA horizontal nav mixes direct links, submenu buttons and neutral zones. The effect signals the nature of each target without the element itself reacting.
WhyThe diamond on links and the neutral shape on non-interactive zones signal the available action — feedback that CSS hover transitions cannot reproduce.
SettingsAssign data-morph="button" to submenu triggers, data-morph="link" to direct links. Lerp at 0.20 for a tight response on fast navigation targets.
Product configurator — swatches, quantity field and size guides — Cursor Morph example 3

③ Product configurator — swatches, quantity field and size guides

WhenA product page offers clickable swatches, a quantity input and size guide links. Three types of elements, three expected cursor behaviors.
WhyThe adaptive cursor encodes each zone's nature without extra icons or tooltips. The interaction decision is made before the click, from the cursor shape alone.
SettingsSet data-morph="button" on swatches and add-to-cart buttons, data-morph="input" on the quantity field. Lerp at 0.20 on small targets — too long a trail hurts precision.

How it works

As soon as the pointer enters #zone-morph, the code adds the class cursor-active and switches #cursorMorph from display: none to display: block. It is the class cursor-active — which you must pair with cursor: none in your CSS — that hides the native cursor inside the zone. #cursorMorph is a fixed-positioned <div> that serves as the replacement cursor. On exit, the class is removed, the cursor hidden and its shape classes reset. The system cursor resumes immediately.

On every mousemove, the code reads e.target.getAttribute('data-morph') — the attribute set on the element directly under the pointer. Based on the value: the class morph-link renders #cursorMorph as 12 × 12 px with no border-radius, rotated 45° — an indigo diamond. The class morph-input applies 2 × 24 px rounded to 1 px, paired with an animation that holds opacity at 1 for the first half of the cycle, then cuts it to 0 with a hard jump — a reproduction of the native text cursor. Without a data-morph attribute, no morph class is added. Before each assignment, className is reset: no residual class survives. The shape change is instantaneous — a plain CSS class swap.

The position tracks the mouse via a requestAnimationFrame loop and a 15% lerp: on each frame, the current position advances 15% of the remaining gap. The cursor never sticks exactly to the pointer — it converges quickly with a slight, perceptible lag. Centering is recomputed on every frame from offsetWidth and offsetHeight: when the shape switches from the 12 × 12 diamond to the 2 × 24 bar, the anchor point self-corrects automatically.

Accessibility

  • Hiding the system cursor is a heavy accessibility decision. Some users rely on a high-visibility OS cursor — large size, high contrast, custom shape set through system preferences. cursor: none wipes all of those adaptations without distinction. Limit its scope to the exact demo zone via the cursor-active class and never apply it to real form fields or any element where a text cursor is expected.
  • prefers-reduced-motion: absent from the code. Two animations run simultaneously: the continuous lerp via requestAnimationFrame and the cursor-blink animation on the input state. Gate the loop initialization behind window.matchMedia('(prefers-reduced-motion: reduce)') and neutralize the CSS animation under the corresponding media query before deploying.
  • The effect is 100% mouse-driven. On touch screens, neither mouseenter nor mousemove fires: the custom cursor stays hidden, the native cursor is unaffected. Verify that every element marked with data-morph remains interactive without the effect: the attribute does not substitute for a :focus state or keyboard behavior.
  • The custom cursor is a purely visual <div> with no ARIA role and no text content. Assistive technologies ignore it. Never carry functional information through cursor shape: a blinking bar does not tell a screen reader that a field is active, and a diamond does not substitute for a :focus-visible state on the link itself.

Browser compatibility

Relies on addEventListener, getAttribute, classList, requestAnimationFrame and offsetWidth — APIs available everywhere for years. The real floor is ES2015 syntax (arrow functions, IIFE): IE 11 stops on a syntax error before the zone is ever entered. No CSS vendor prefixes required.

Chrome 80+✓ Full
Firefox 75+✓ Full
Safari 13+✓ Full
Edge 80+✓ Full
Mobile iOS✓ Inert (touch)
Android Chrome✓ Inert (touch)

Without JavaScript, <code>#cursorMorph</code> stays at <code>display: none</code> and the system cursor displays normally — no functional content depends on the custom cursor. Key watch-out: if <code>cursor: none</code> is declared in CSS without being conditioned on the <code>cursor-active</code> class, the native cursor disappears even without the effect. Always write the rule as <code>.cursor-active { cursor: none; }</code>, never on the zone element alone.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Lerp factor (0.15 in the JS) 0.15 Controls the cursor's trail. 0.05 = arrives noticeably behind the mouse. 0.30 = almost sticks to the pointer. 1.0 = instant tracking, no smoothing. On small targets (swatches, icons), raise it to 0.20–0.25 to preserve click precision.
Expected data-morph values 'button', 'link', 'input' The code reads getAttribute('data-morph') and adds the matching class. For a new state — morph-video on a playback zone for example — add the branch in the mousemove handler and define the shape in CSS. Without the attribute, the cursor takes its default shape.
Link state shape (12 × 12 px, rotate 45°) 12 px × 12 px, rotated 45° These CSS values define the diamond. Increase the size for a larger diamond, remove the rotation for a square. offsetWidth/offsetHeight is re-read on every frame: changing dimensions in CSS requires no adjustment in the JavaScript.
Input state shape (2 × 24 px, border-radius 1 px) 2 px × 24 px These dimensions mimic the native text cursor. Increase the height for large text areas; thicken to 4–6 px for a stylized cursor. The offsetWidth/offsetHeight recalculation guarantees correct centering regardless of the chosen geometry.
Shared color (background: #6366f1) #6366f1 (indigo) The three morph classes each declare their color in their own background rule. For centralized control, replace the hard-coded values with a CSS variable --cursor-color defined at root and referenced in each morph selector.
cursor-blink animation (1 s, hard cut at 50%) 1 s, opaque 0–50%, invisible 51–100% The keyframes cut opacity to 0 at mid-cycle — no progressive fade. For a snappier blink, reduce to 0.6 s; for a fade, rewrite the keyframes with linear interpolation. Add animation: none under prefers-reduced-motion to suppress blinking without touching the shape.

FAQ

The native cursor only disappears if you pair cursor: none with the cursor-active class in your CSS — the JavaScript does not touch that property. Without that rule, the custom cursor overlays the system cursor and both are visible at the same time. The separation is intentional: the scope of cursor: none stays under your control, not hard-coded into the script.
getAttribute('data-morph') returns null when the attribute is absent. The code then resets className to 'custom-cursor cursor-morph' without adding any morph class: the cursor falls back to its default shape. This reset happens on every mousemove regardless of the target — no residual class from a previous hover survives on the cursor element.
No — and this is a limitation to anticipate. Once an <input> receives focus and the user starts typing, mousemove stops firing if the pointer does not move. The custom cursor freezes at its last computed position. Make sure cursor: none does not apply to form fields in production: users must see the native text cursor as soon as they type.