Customising Mouse Cursors
Learn how to control cursor visibility, switch between default system cursors, and create custom cursors
When we're working on a mouse-based game, creating an engaging user interface typically requires us to change the visual appearance of the cursor.
For example, we might customize the cursor to fit the theme of our game. We may also want to apply further modifications to the cursor based on what the user is pointing at, like changing the cursor to a sword if the pointer is hovering over an enemy. This helps players quickly understand what effect clicking will have.
This lesson will guide you through the various cursor customization options available in SDL3. We'll cover:
- Simple visibility toggles to show and hide the cursor
- Switching between a range of default cursor options provided by the platform
- Implementing custom cursors from image files
- Completely replacing the cursor implementation to take full control on how mouse interaction is implemented
Starting Point
This lesson builds on our earlier work. To focus on cursor customization, we will start with a minimal project containing a Window class and a main function with a basic application loop.
Files
Showing and Hiding the Cursor
In SDL3, cursor visibility is controlled by two distinct functions: SDL_ShowCursor() and SDL_HideCursor():
// Show the Cursor
SDL_ShowCursor();
// Hide the Cursor
SDL_HideCursor();Handling Errors When Toggling the Cursor
Both SDL_ShowCursor() and SDL_HideCursor() return true on success and false on failure. This allows us to check for errors and use SDL_GetError() for an explanation:
if (!SDL_HideCursor()) {
std::cout << "Error hiding cursor: "
<< SDL_GetError();
}Querying Cursor Visibility
We can use SDL_CursorVisible() to determine the current visibility of the cursor without changing it. It returns true if the cursor is visible and false otherwise:
if (SDL_CursorVisible()) {
std::cout << "Cursor is visible";
} else {
std::cout << "Cursor is hidden";
}Cursor is visiblePremade System Cursors
We pass an SDL_SystemCursor enum value describing which cursor we'd like to create. For example, we can create the default arrow cursor by passing SDL_SYSTEM_CURSOR_DEFAULT:
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);The value returned by SDL_CreateSystemCursor() is an SDL_Cursor pointer (an SDL_Cursor*):
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT)
};We should treat these SDL_Cursor objects as opaque - that is, they are not designed for us to read or modify their properties. However, we still need these SDL_Cursor pointers for use with other SDL functions that we'll cover later.
Using SDL_SystemCursor Values
The SDL_SystemCursor enum includes a set of values for common cursors. Note that the names have changed from SDL2 to align with CSS cursor names. Its possible values are as follows:
SDL_SYSTEM_CURSOR_DEFAULT: The default cursor, usually an arrow.SDL_SYSTEM_CURSOR_POINTER: An alternative cursor designed to look like a hand.SDL_SYSTEM_CURSOR_CROSSHAIR: A cursor designed to look like a crosshair.SDL_SYSTEM_CURSOR_TEXT: The I-beam cursor, typically used for text editing.SDL_SYSTEM_CURSOR_PROGRESS: A cursor designed to indicate something is loading.SDL_SYSTEM_CURSOR_NOT_ALLOWED: Used to indicate blocked or disabled actions.
Most systems have cursors designed to indicate resizing actions. We can load these system cursors using the following enum values:
SDL_SYSTEM_CURSOR_NWSE_RESIZE: Double arrow pointing northwest and southeast.SDL_SYSTEM_CURSOR_NESW_RESIZE: Double arrow pointing northeast and southwest.SDL_SYSTEM_CURSOR_EW_RESIZE: Double arrow pointing east and west.SDL_SYSTEM_CURSOR_NS_RESIZE: Double arrow pointing north and south.SDL_SYSTEM_CURSOR_MOVE: Four-pointed arrow pointing north, south, east, and west.
Errors When Creating System Cursors
It is possible for the SDL_Cursor pointer returned by SDL_CreateSystemCursor() to be a nullptr. This represents an error, and we can call SDL_GetError() for an explanation:
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT)
};
if (!Cursor) {
std::cout << "Failed to create cursor: "
<< SDL_GetError();
}Setting Cursors
Once we've created a cursor and have access to it through an SDL_Cursor pointer, we can instruct SDL to use that cursor for our mouse. We do this by passing the pointer to SDL_SetCursor():
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT)
};
SDL_SetCursor(Cursor);Freeing Cursors
When we create a cursor using a function like SDL_CreateSystemCursor(), SDL allocates the memory required for that cursor and manages it internally.
However, when we no longer need that cursor, we should communicate that to SDL so it can free the memory and prevent leaks. In SDL3, we do this by passing the SDL_Cursor* to SDL_DestroyCursor():
// Create the cursor
SDL_Cursor* Cursor{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT)
};
// Use the cursor...
// Free it
SDL_DestroyCursor(Cursor);Note this only applies to cursors we create using functions like SDL_CreateSystemCursor() or SDL_CreateColorCursor(). Every invocation of a cursor creation function should be matched with an invocation to SDL_DestroyCursor(). We cover in more detail in our introductory course.
Functions that retrieve a pointer to an existing SDL_Cursor, such as SDL_GetCursor() and SDL_GetDefaultCursor(), do not allocate new memory, so we do not need to free the pointer they return.
Getting the Current Cursor
We can get the cursor we are currently using SDL_GetCursor(). This will return the SDL_Cursor pointer applied from our most recent call to SDL_SetCursor(), or the default cursor if we never changed it:
SDL_Cursor* Cursor{
SDL_GetCursor()
};We can use this to check if a specific cursor is currently active:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Cursor* Crosshair{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)
};
if (SDL_GetCursor() != Crosshair) {
std::cout << "Crosshair cursor is not active";
}
SDL_SetCursor(Crosshair);
if (SDL_GetCursor() == Crosshair) {
std::cout << "\nBut now it is";
}
SDL_Event Event;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_EVENT_QUIT) {
IsRunning = false;
}
}
GameWindow.Render();
GameWindow.Update();
}
SDL_Quit();
return 0;
}Crosshair cursor is not active
But now it isGetting the Default Cursor
We can get a pointer to the system's default cursor using the SDL_GetDefaultCursor() function:
SDL_Cursor* DefaultCursor{
SDL_GetDefaultCursor()
};This is primarily used when we need to revert a change we made using SDL_SetCursor():
SDL_Cursor* Crosshair{
SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR)
};
// Use a non-default cursor
SDL_SetCursor(Crosshair);
// ...
// Revert to the default
SDL_SetCursor(SDL_GetDefaultCursor());If SDL_GetDefaultCursor() fails, it will return a nullptr, and we can call SDL_GetError() to understand why:
SDL_Cursor* DefaultCursor{
SDL_GetDefaultCursor()
};
if (!DefaultCursor) {
std::cout << "Unable to get default cursor: "
<< SDL_GetError();
}It's not necessary to call SDL_DestroyCursor() on the default cursor, but it is safe to do so.
Creating a Custom Cursor
We'll walk through the underlying mechanics for creating custom cursors here, but will create a full implementation in a dedicated class in the next section.
The first step of creating a custom cursor is to get the pixel data for that cursor into an SDL_Surface.
Remember, we can use the SDL3_image library to load an image file into an SDL_Surface:
SDL_Surface* Surface{IMG_Load("cursor.png")};
if (!Surface) {
std::cout << "IMG_Load Error: " << SDL_GetError();
}
// ...
// Remember to free resources when no longer needed
SDL_DestroySurface(Surface);Once we have our SDL_Surface, we can create a cursor from it using the SDL_CreateColorCursor() function. We pass the SDL_Surface pointer as the first argument, in addition to two integers representing horizontal (x) and vertical (y) coordinates:
SDL_Cursor* Cursor{SDL_CreateColorCursor(
Surface, 10, 20
)};
SDL_SetCursor(Cursor);
// ...
// Remember to free resources when no longer needed
SDL_DestroyCursor(Cursor)The integer x and y arguments tell SDL where the "hot spot" is within the cursor image. The hot spot is the exact part of the cursor image that corresponds to what the user is pointing at.
For example, if our cursor was an image of an arrow, the hot spot would be the exact tip of that arrow. For a cursor designed to look like a hand, the hot spot would be the tip of the finger.

The x value is the distance of this pixel from the left edge of the image, and the y value is the distance from the top edge.
Errors When Creating Cursors
If SDL_CreateColorCursor() is unable to create a cursor, it will return a nullptr. We can react accordingly to this, and call SDL_GetError() if we want an explanation of what went wrong:
SDL_Cursor* Cursor{SDL_CreateColorCursor(
nullptr, // Passing nullptr as surface
10, 20
)};
if (!Cursor) {
std::cout << "Error creating cursor: "
<< SDL_GetError();
}Error creating cursor: Parameter 'surface' is invalidCreating a Cursor Class
The following class combines these image and cursor management techniques behind a friendly interface:
src/Cursor.h
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <iostream>
#include <string>
class Cursor {
public:
Cursor(const std::string& Path, int HotX, int HotY)
: CursorSurface{IMG_Load(Path.c_str())},
CursorPtr{nullptr},
HotspotX{HotX},
HotspotY{HotY}
{
if (!CursorSurface) {
std::cout << "IMG_Load Error: "
<< SDL_GetError() << '\n';
return;
}
CursorPtr = SDL_CreateColorCursor(
CursorSurface, HotspotX, HotspotY
);
if (!CursorPtr) {
std::cout << "SDL_CreateColorCursor Error: "
<< SDL_GetError() << '\n';
SDL_DestroySurface(CursorSurface);
CursorSurface = nullptr;
}
}
void Activate() const {
if (CursorPtr) {
SDL_SetCursor(CursorPtr);
}
}
static void Deactivate() {
SDL_SetCursor(SDL_GetDefaultCursor());
}
bool IsValid() const {
return CursorPtr != nullptr;
}
// Memory management
~Cursor() {
if (CursorPtr) {
SDL_DestroyCursor(CursorPtr);
}
if (CursorSurface) {
SDL_DestroySurface(CursorSurface);
}
}
// Disabling copying to simplify memory management
Cursor(const Cursor&) = delete;
Cursor& operator=(const Cursor&) = delete;
private:
SDL_Cursor* CursorPtr;
SDL_Surface* CursorSurface;
int HotspotX, HotspotY;
};The following program uses this class to create a custom cursor. The cursor is applied when the player presses 1, and reverted to the default when they press 2:
Files
Bespoke Cursor Rendering
We're not forced to use SDL's mechanisms for cursor customization if we don't want to. We can take on full responsibility for cursor rendering within our code. This allows us to do more advanced things with the cursor, such as animation or physics-based motion.
To set this up, we'd hide the system-managed cursor using SDL_HideCursor(), and then render our object on the screen using, for example, the surface blitting techniques we covered earlier.
We'd update the blitting position based on SDL_EVENT_MOUSE_MOTION events coming through our event loop, or SDL_GetMouseState() calls within a tick function. We cover this in more detail in our lesson on .
Below, we've updated our Cursor class to use this technique. We also included a Tick() function as an example, but it's not currently doing anything:
src/Cursor.h
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <iostream>
#include <string>
class Cursor {
public:
Cursor(std::string Path, int HotX, int HotY)
: CursorSurface{IMG_Load(Path.c_str())},
HotspotX{HotX},
HotspotY{HotY} {
if (!CursorSurface) {
std::cout << "IMG_Load Error: "
<< SDL_GetError();
}
}
~Cursor() {
if (CursorSurface) {
SDL_DestroySurface(CursorSurface);
}
}
void Tick() {
// Do tick things - eg animation
// ...
}
void HandleMouseMotion(
const SDL_MouseMotionEvent& E
) {
// Update the cursor position such that the next
// render blits the image in the correct place
CursorX = E.x;
CursorY = E.y;
}
void Render(SDL_Surface* targetSurface) const {
if (!CursorSurface || !targetSurface) {
return;
}
// Position the blitting such that the image's
// hotspot matches the last reported cursor position
SDL_Rect Destination{
(int)(CursorX - HotspotX),
(int)(CursorY - HotspotY),
0, 0
};
SDL_BlitSurface(
CursorSurface, nullptr,
targetSurface, &Destination
);
}
private:
SDL_Surface* CursorSurface;
int HotspotX, HotspotY;
float CursorX = 0.0f, CursorY = 0.0f;
};Our event loop now needs to forward mouse motion events to our custom Cursor object, and our application loop needs to Tick() and Render() the cursor at the appropriate time.
Here's an example:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Cursor.h"
#include "Window.h"
int main(int, char**) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Cursor CustomCursor{"cursor.png", 16, 16};
SDL_HideCursor();
SDL_Event E;
bool IsRunning = true;
while (IsRunning) {
while (SDL_PollEvent(&E)) {
if (E.type == SDL_EVENT_QUIT) {
IsRunning = false;
} else if (E.type == SDL_EVENT_MOUSE_MOTION) {
CustomCursor.HandleMouseMotion(E.motion);
}
}
CustomCursor.Tick();
GameWindow.Render();
CustomCursor.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}Note that, whilst rendering the cursor through our application loop gives us more flexibility, there is a downside compared to using the native cursor rendering approach provided by functions like SDL_CreateColorCursor() and SDL_SetCursor().
When we render the cursor as part of our regular game loop, it means cursor updates are constrained by how fast our loop can output each frame.
Reduced frame rates are particularly noticeable to players when it affects their cursor motion, so we should be careful here. We should only use this approach if we require the more advanced capabilities it unlocks, or if we're certain our application will run quickly enough to ensure the user's mouse actions remain highly responsive.
Summary
In this lesson, we covered all the main cursor management techniques available within SDL3. Here are the key points:
- Controlling cursor visibility using
SDL_ShowCursor()andSDL_HideCursor(). - Using system-provided cursors through
SDL_CreateSystemCursor(). - Managing cursor resources with proper cleanup using
SDL_DestroyCursor(). - Creating custom cursors from images with
SDL_CreateColorCursor(). - Understanding cursor hotspots and their importance.
- Creating a reusable
Cursorclass for better organization. - Implementing custom cursor rendering for advanced effects.