Skip to content

Instantly share code, notes, and snippets.

@jnicol
Last active June 2, 2026 04:59
Show Gist options
  • Select an option

  • Save jnicol/75aa0a1a0939974c201bba3881b9bd81 to your computer and use it in GitHub Desktop.

Select an option

Save jnicol/75aa0a1a0939974c201bba3881b9bd81 to your computer and use it in GitHub Desktop.
CSS/JS cursor effects
<html>
<body>
<style>
/*
Cursor follow + fill effect inspired by
https://juno-hamburg.com/
and
https://dmmotionarts.com/button-fill-at-cursor-location-on-hover-elementor-and-js-tutorial/
There are 3 separate effects:
1. Custom cursor that follows mouse movements
2. Cursor grows on hover and displays a text label
3. Button fill effect on hover, where a circle expands from the cursor position to fill the button.
Demo: https://jonathannicol.com/projects/cursor-fill/
*/
/*
Custom cursor
---------------------------------------
*/
body {
background-color: white;
}
@media not (any-pointer: coarse) {
html:not(.no-js) body * {
cursor: none;
}
html:not(.no-js) {
cursor: none;
}
}
html:not(.no-js) .cursor.hide {
visibility: hidden
}
.cursor {
--top: -5rem; /* initially off screen */
--left: -5rem;
}
/* Two circles, to achieve desired mix blend mode */
.cursor-bg,
.cursor-fg {
position: fixed;
top: var(--top);
left: var(--left);
width: 4rem;
height: 4rem;
border-radius: 50%;
pointer-events: none;
overflow: hidden;
transform: translate3d(-50%, -50%, 0) scale(.375); /* small, so we can grow it on hover */
transform-origin: center;
transition: transform .2s cubic-bezier(.23, 1, .32, 1);
}
/* mix-blend modes are optional. You could remove .cursor-bg entitely and give .cursor-fg a solid color if you want */
.cursor-bg {
z-index: 100;
background-color: white;
mix-blend-mode: exclusion;
}
.cursor-fg {
z-index: 101;
background-color: #5048fe;
mix-blend-mode: lighten;
display: flex;
align-items: center;
justify-content: center;
}
/*
Grow/hover label effect
---------------------------------------
*/
.cursor.grow .cursor-bg,
.cursor.grow .cursor-fg {
transform: translate3d(-50%, -50%, 0) scale(1);
}
/* text inside cursor */
.cursor-text {
color: white;
text-transform: uppercase;
display: none;
}
html:not(.no-js) .cursor.grow .cursor-text {
display: inline-block;
}
/* disable mix-blend mode when button grows to show a label */
.cursor.grow .cursor-fg {
background-color: #5048fe;;
mix-blend-mode: initial;
}
/*
Button fill effect
---------------------------------------
*/
.cursor-fill {
position: relative;
display: inline-block;
overflow: hidden;
}
.cursor-fill-bg {
position: absolute;
width: 100%;
top: 0%;
left: 0%;
background-color: #5048fe;
z-index: 1;
border-radius: 50%;
transform: translate(-50%,-50%) scale(0);
transition: transform 0.2s cubic-bezier(.23, 1, .32, 1);
}
.cursor-fill:hover .cursor-fill-bg {
transform: translate(-50%,-50%) scale(2.5);
}
.cursor-fill-content {
position: relative;
z-index: 2;
}
/*
Demo styles. Not required for effects
---------------------------------------
*/
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
margin: 0;
}
.btn {
padding: 0.5rem 2rem;
background-color: transparent;
border: 2px solid black;
margin: 0 1rem;
}
.btn:hover {
color: white;
border-color: #5048fe;
}
.btn--round {
padding: 1rem;
aspect-ratio: 1/1;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.cursor-grow {
display: flex;
align-items: center;
justify-content: center;
background: #ddd;
padding: 5rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Cursor follow effect
// --------------------
const cursor = document.querySelector('.cursor');
const cursorLabel = document.querySelector('.cursor-text');
const mouse = {x: 0, y: 0};
// Track cursor movements
window.addEventListener('mousemove', (event) => {
mouse.x = event.clientX;
mouse.y = event.clientY;
requestAnimationFrame(() => updateCursor());
});
// request animation frame recursive function to update cursor position
const updateCursor = () => {
cursor.style.setProperty('--left', `${mouse.x}px`);
cursor.style.setProperty('--top', `${mouse.y}px`);
}
// Hide cursor when mouse leaves viewport
document.documentElement.addEventListener("mouseleave",()=>{
cursor.classList.add("hide");
});
document.documentElement.addEventListener("mouseenter",()=>{
cursor.classList.remove("hide");
});
// Cursor grow effect
// --------------------
const cursorGrows = document.querySelectorAll('.cursor-grow');
cursorGrows.forEach(cursorGrow => {
cursorGrow.addEventListener('mouseenter', () => {
cursor.classList.add('grow');
label = cursorGrow.dataset.cursorLabel;
cursorLabel.textContent = label ? label : '';
});
cursorGrow.addEventListener('mouseleave', () => {
cursor.classList.remove('grow', 'view');
cursorLabel.textContent = '';
});
});
// Fill effect
// ------------------
const cursorFills = document.querySelectorAll('.cursor-fill');
cursorFills.forEach(cursorFill => {
cursorFill.addEventListener('mouseenter', e => {
cursor.classList.add('hide'); // hide custom cursor when hovering button
updateFillPos(e, cursorFill);
});
cursorFill.addEventListener('mouseleave', e => {
cursor.classList.remove('hide'); // hide custom cursor when hovering button
updateFillPos(e, cursorFill);
});
});
function updateFillPos(e, cursorFill) {
const fillBg = cursorFill.querySelector('.cursor-fill-bg');
const rect = cursorFill.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
fillBg.style.left = `${x}px`;
fillBg.style.top = `${y}px`;
fillBg.style.height = cursorFill.clientWidth + 'px';
}
});
</script>
<div class="cursor">
<div class="cursor-bg"></div>
<div class="cursor-fg"><div class="cursor-text"></div></div>
</div>
<div class="cursor-grow" data-cursor-label="View">Hover me</div>
<div class="cursor-fill btn">
<div class="cursor-fill-bg"></div>
<div class="cursor-fill-content">Button</div>
</div>
<div class="cursor-fill btn btn--round">
<div class="cursor-fill-bg"></div>
<div class="cursor-fill-content">Button</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment