Handcrafted Mascot On My Blog Like Its 2007 (feat. VanillaJS)
You know those cute little mascots that follow your cursor like needy pets? I wanted one. So I built it. No frameworks, no libraries, no npm, no bs.
Just pure VanillaJS and some emotional support from chatgpt (read as: hair scratching because I think I would’ve done it faster by taking support from stackoverflow instead).
The [cyb]org to my oxal <3 :

Step 1: Create a Sprite
I asked chatgpt to create a cute cyborg for me, then I put it in piskel editor and edited it heavily, changed the hands, legs, clothes, eyes, animated it. Basically broke it apart and fixed it.
const cyborgImgSprite = "/static/img/cyborg-sprite.png"
const SPRITE_CONFIG = {
frames: 9,
frameWidth: 64,
frameHeight: 64,
animationSpeed: 100/1000,
spriteSheet: cyborgImgSprite
};
Nine frames. Because he’s inspired by the way of the nine tales!
Step 2: Some state to remember where our buddy is
let mascotX = 0, mascotY = 0;
let targetX = window.innerWidth / 3;
let targetY = window.innerHeight / 3;
let currentMouseX = 0, currentMouseY = 0;
let currentFrame = 0;
let isFlipped = false;
We track position, target, mouse, animation frame, and whether he’s moonwalking. Globals are not evil if you use them like this.
Step 3: Follow the mouse and jump on click
function setupEventListeners(mascot) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('click', handleClick);
}
function handleMouseMove(e) {
currentMouseX = e.clientX;
currentMouseY = e.clientY;
}
function handleClick(e) {
targetX = e.clientX;
targetY = e.clientY;
}
Mouse moves? He looks. Click? He lunges.
Step 4: Sometimes he just needs to move
Go listen to dancin krono remix. N>o<W
function moveEvery5Seconds(mascot, dt) {
lastMoveDt += dt;
if (lastMoveDt >= MOVE_INTERVAL) {
lastMoveDt = 0;
if (Math.abs(lastMoveX - mascotX) > 10 || Math.abs(lastMoveY - mascotY) > 10) {
const directionX = Math.random() > 0.5 ? 1 : -1;
targetX = mascotX + 70 * directionX;
isFlipped = directionX < 0;
mascot.style.transform = isFlipped ? 'translate(-50%, -50%) scaleX(-1)' : 'translate(-50%, -50%) scaleX(1)';
}
lastMoveX = mascotX;
lastMoveY = mascotY;
}
}
If left alone, he shuffles side to side every few seconds like he’s trying to get your attention.
Step 5: Glide toward the target like a smooooooooth criminal
function updateMascotPosition(mascot, dt) {
mascotX += (targetX - mascotX) * EASING * dt;
mascotY += (targetY - mascotY) * EASING * dt;
if (mascotX > window.innerWidth || mascotX < 0) targetX = window.innerWidth / 2;
if (mascotY > window.innerHeight || mascotY < 0) targetY = window.innerHeight / 2;
}
function drawMascot(mascot, dt) {
mascot.style.left = mascotX + 'px';
mascot.style.top = mascotY + 'px';
}
CSS left and top. You’ve done this in 2007, you can do it again.
Step 999: Animate the sprite so it’s not a cardboard cutout
function updateSpriteAnimation(mascot, dt) {
lastAnimationDt += dt;
if (lastAnimationDt >= SPRITE_CONFIG.animationSpeed) {
lastAnimationDt = 0;
currentFrame = (currentFrame + 1) % SPRITE_CONFIG.frames;
const bgPositionX = currentFrame * SPRITE_CONFIG.frameWidth;
mascot.style.backgroundPosition = `-${bgPositionX}px 0`;
}
}
Frame changes = walking illusion. Your cyborg is alive. ALIVE.
Step 9000: Tick tock tick tock
function ticker(mascot) {
const currentTime = Date.now();
const dt = (currentTime - prevFrameTime) / 1000;
prevFrameTime = currentTime;
updateMascotPosition(mascot, dt);
moveEvery5Seconds(mascot, dt);
updateSpriteAnimation(mascot, dt);
drawMascot(mascot, dt);
requestAnimationFrame(() => ticker(mascot));
}
The game loop. Every sufficiently large program ends up reimplementing either ECS or a game loop.
Step -1: And finally
function createMascotElement(spriteConfig) {
const mascot = document.createElement('div');
mascot.id = 'cursor-mascot';
const style = document.createElement('style');
style.textContent = `
#cursor-mascot {
position: fixed;
width: ${spriteConfig.frameWidth}px;
height: ${spriteConfig.frameHeight}px;
background-image: url('${spriteConfig.spriteSheet}');
background-repeat: no-repeat;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: transform 0.1s ease;
}
`;
document.head.appendChild(style);
return mascot;
}
The DOM!
Step 0: Init!
function initMascot() {
const mascot = createMascotElement(SPRITE_CONFIG);
document.body.appendChild(mascot);
setupEventListeners(mascot);
ticker(mascot);
}
document.addEventListener('DOMContentLoaded', initMascot);
Result?
You’ve got an animated cyborg mascot that follows clicks, moves on its own, and flips direction like a moody anime character. All in ~100 lines of JavaScript.
No react components were harmed in the making of this blog post. I promise.