Skip to main content

How to Add a Custom Cursor

This tutorial was written in February 2026, for v2 of the engine.

Most games look better with a custom cursor. The default browser arrow screams "webpage" — it breaks the whole pixel-art vibe. Swapping it out for your own sprite is quick and makes a big difference.

We need three things: hide the default cursor, define a cursor sprite, and draw it at the mouse position every frame.

Hiding the Default Cursor

Call cursor(false) in your init() function to hide the browser cursor. Under the hood, it sets CSS cursor: none on the canvas.

// inside init()
cursor(false);

// the browser cursor is now hidden over the canvas
// call cursor() to restore it, or cursor('pointer') for a CSS cursor
Custom Cursor: Hide Cursor
Move your mouse over the canvas — the default cursor disappears

You can bring it back any time — cursor() restores the default arrow, and cursor('pointer') or any other CSS cursor value works too. Handy if your game has menus that feel better with a normal cursor.

Defining a Cursor Sprite

Sprites in Floaty are 8x8 arrays of color values. Each number maps to a palette color (0–15), and -1 means transparent. Here's an arrow cursor — 7 (white) fills the shape, 0 (black) outlines it:

const sprites = {
    cursor: [
         7,  0, -1, -1, -1, -1, -1, -1,
         7,  7,  0, -1, -1, -1, -1, -1,
         7,  7,  7,  0, -1, -1, -1, -1,
         7,  7,  7,  7,  0, -1, -1, -1,
         7,  7,  7,  7,  7,  0, -1, -1,
         7,  7,  0,  0,  0,  0, -1, -1,
         7,  0,  0, -1, -1, -1, -1, -1,
         0,  0, -1, -1, -1, -1, -1, -1,
    ],
};
Custom Cursor: Define Sprite
The arrow cursor sprite rendered from its pixel data at the center of the screen

Read the array top to bottom, left to right, and you can see the arrow take shape: a single pixel at the top-left, widening row by row, then tapering back. We pass the sprites object to start() so the engine knows about it.

Tip: You can design sprites visually in the sprite editor — draw your cursor pixel by pixel, then copy the array with Ctrl+C (or Cmd+C on Mac).

Drawing the Cursor

Now we just need to draw it. Call mouse() to get the current position as { x, y }, then spr() to draw the sprite there:

engine.scope(({ start, cls, spr, mouse, cursor }) => {
    const sprites = {
        cursor: [
             7,  0, -1, -1, -1, -1, -1, -1,
             7,  7,  0, -1, -1, -1, -1, -1,
             7,  7,  7,  0, -1, -1, -1, -1,
             7,  7,  7,  7,  0, -1, -1, -1,
             7,  7,  7,  7,  7,  0, -1, -1,
             7,  7,  0,  0,  0,  0, -1, -1,
             7,  0,  0, -1, -1, -1, -1, -1,
             0,  0, -1, -1, -1, -1, -1, -1,
        ],
    };

    function init() {
        cursor(false);
    }

    function draw() {
        cls(12);
        const pos = mouse();
        spr('cursor', pos.x, pos.y);
    }

    start({ sprites, sounds: {}, init, draw, target });
});
Custom Cursor: Draw Cursor
Move your mouse to see the custom arrow cursor follow

The engine converts screen coordinates to canvas coordinates for you, so mouse() returns values in the same 128x128 pixel space you draw in. The sprite's top-left corner lands on the mouse position — that feels natural for an arrow since the "tip" is at the top-left.

Going Further

Not every cursor shape works with top-left alignment. A crosshair should be centered on the mouse position — subtract half the sprite size from each coordinate:

const sprites = {
    crosshair: [
        -1, -1, -1,  7, -1, -1, -1, -1,
        -1, -1, -1,  7, -1, -1, -1, -1,
        -1, -1, -1,  7, -1, -1, -1, -1,
         7,  7,  7, -1,  7,  7,  7, -1,
        -1, -1, -1,  7, -1, -1, -1, -1,
        -1, -1, -1,  7, -1, -1, -1, -1,
        -1, -1, -1,  7, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1,
    ],
};

// in draw(), center the crosshair on the mouse position:
const pos = mouse();
spr('crosshair', pos.x - 3, pos.y - 3);
Custom Cursor: Crosshair
Move your mouse — the crosshair is centered on the mouse position instead of top-left aligned

A few directions you could take this:

  • Animated cursors — define multiple sprite frames and cycle through them with a counter
  • Context-sensitive cursors — swap the sprite based on what the mouse is over (a hand for buttons, a crosshair when aiming)
  • Cursor trails — store the last few mouse positions in an array and draw faded copies of the sprite at each one