native_mouse_cursor 1.0.2
native_mouse_cursor: ^1.0.2 copied to clipboard
Turn any image, SVG, or painted glyph into a real OS mouse cursor on Flutter desktop, web & Android.
native_mouse_cursor #
Turn any image, SVG, or painted glyph into a real OS mouse cursor โ on Flutter desktop, web & Android.
๐ Try the live web demo โ (web preview โ full native cursors shine on macOS / Windows / Linux / Android)
Unlike a cursor "painted" inside Flutter (a widget that chases the pointer), a
NativeMouseCursor is handed to the operating system, so the OS compositor
draws it for you. ๐ช
โจ Why use it #
- โก Zero lag โ tracks the hardware pointer exactly, with no one-frame trail.
- ๐ซง No jitter โ a shadow or glow baked into the bitmap never shimmers, even while the cursor rotates.
- ๐ Drop-in โ it's a real
MouseCursor, so it works anywhere aSystemMouseCursorsvalue does (MouseRegion,InkWell, scrollbars, โฆ). - ๐ Rotation & mirroring โ spin a glyph by angle or flip it on demand; each variant is baked and cached automatically.
- ๐ Baked drop shadows โ CSS-style shadows rendered into the bitmap, so they stay rock-steady at every angle.
- ๐ฅ๏ธ HiDPI-crisp โ bakes at your device pixel ratio and re-bakes on change.
- ๐๏ธ Optional painted overlay โ on web/desktop, opt into an in-app overlay that hides the system cursor and paints a perfectly seamless per-region one.
- ๐ฆ SPM-first on macOS โ no CocoaPods required.
๐งฉ Platform support #
| Platform | Backend | Status | |
|---|---|---|---|
| macOS | NSCursor (Swift, SPM) |
โ Supported | |
| Windows | HCURSOR (Win32) |
โ Supported | |
| Linux | GdkCursor (GTK) |
โ Supported | |
| Android | PointerIcon (API 24+) |
โ Supported ยฒ | |
| Web | CSS url(...) cursor |
โ Supported ยน | |
| iOS / iPadOS | system pointer | โ Not possible ยณ |
ยน Each cursor is applied as a CSS cursor: url(...) value, sized in logical
px and capped at 128 px (browsers draw a cursor image at its intrinsic pixels and
Chrome ignores larger ones). For HiDPI crispness it also emits a
device-resolution image via image-set(โฆ 2x), with the plain url() as a
fallback. For a perfectly seamless per-region cursor, wrap your app in
NativeMouseCursorOverlay(force: true) to paint
the glyph and hide the CSS cursor instead.
ยฒ Native PointerIcon for tablets/Chromebooks with a connected mouse,
trackpad or stylus on API 24+. On older devices the system pointer is used.
For a rotating cursor, prefer the
painted overlay โ rapid PointerIcon swaps
flicker on Android.
ยณ iPadOS draws and manages the pointer itself โ there's no API to install an arbitrary bitmap cursor, nor to hide the system pointer, so the system pointer is used. (A painted overlay would just show through it as a double cursor.) iPhone is touch-only โ no pointer to replace.
๐ฆ Install #
dependencies:
native_mouse_cursor: ^1.0.1
flutter pub add native_mouse_cursor
๐ Quick start #
The whole API is: register a source under an id, then get it. ๐ฏ
Everything hard โ loading the glyph, rotation, the baked drop shadow, automatic bitmap sizing, the angle-keyed cache, background warming and DPR re-baking โ lives in the package.
Mix NativeMouseCursorMixin into your State and the rest is automatic: it
points the cache at the context's devicePixelRatio (re-baking on a DPR change)
and rebuilds when a cursor finishes baking โ so you can call svg / get
straight from build():
import 'package:native_mouse_cursor/native_mouse_cursor.dart';
class _MyState extends State<MyWidget> with NativeMouseCursorMixin {
@override
void initState() {
super.initState();
// ๐ Register here, NOT in build() โ svg() kicks off an async load + bake,
// so it's a one-time side effect. For an SVG asset that's the whole call;
// size, shadow and the hotspot all default.
NativeMouseCursor.svg('rotate', 'assets/icons/rotate.svg');
// size: defaults to the SVG's own (viewBox) size
// shadow: defaults to x:0 y:1 blur:1.5 black 50% (ฯ=blur/2); null = none
}
@override
Widget build(BuildContext context) {
// ๐ build() only fetches โ the bitmap is baked + cached per angle on
// demand, and the mixin rebuilds when a fresh one lands.
return MouseRegion(
// get() never returns null: until the bitmap is baked it returns
// SystemMouseCursors.basic, so no `??` is needed.
cursor: NativeMouseCursor.get('rotate', angle: handleAngleRadians),
child: handle,
);
}
}
๐ก
NativeMouseCursor.has(id)lets you guard a one-off lazy registration if you can't register up front. Prefer not to use the mixin? CallNativeMouseCursor.configure(devicePixelRatio:, onReady:)yourself once (and again whenever the DPR changes) instead.
๐จ Cursor sources #
Pick the register call that matches your glyph โ all take the same id,
size, shadow and hotspot options:
| Call | Glyph source |
|---|---|
๐ผ๏ธ NativeMouseCursor.svg |
an SVG asset path (re-rasterised from vector) |
๐
NativeMouseCursor.image |
a decoded ui.Image |
โ๏ธ NativeMouseCursor.draw |
a CursorPainter you paint into a box yourself |
๐ ๏ธ NativeMouseCursor.builder |
produce the bitmap yourself per angle + DPR |
NativeMouseCursor.image('pointer', myUiImage, size: const Size(24, 24));
๐ Rotation #
There's no rotation flag โ just the angle you pass to get. A fixed cursor is
simply one you always fetch at the default angle (0), so a single bitmap is baked
and reused:
NativeMouseCursor.svg('resize-h', 'assets/resize-h.svg'); // โ
// ...
cursor: NativeMouseCursor.get('resize-h'),
For a glyph that turns with a handle, vary the angle โ each rotation bucket is baked and cached the first time it's requested (the at-rest angle is warmed in the background; the nearest already-baked angle is shown meanwhile). The bitmap box is always sized for the glyph's diagonal, so it never clips as it turns. ๐
โ๏ธ Mirroring #
flipX / flipY are resolved at get time, so one registered glyph yields a
mirrored pair on demand โ no second asset:
NativeMouseCursor.svg('hand', 'assets/hand-right.svg');
// the same glyph, flipped โ a left hand from the right-hand asset:
cursor: NativeMouseCursor.get('hand', flipX: pointingLeft),
Every (angle, flip) combination is baked and cached the first time it's asked
for; the unflipped variant is warmed in the background.
๐ฏ Hotspot #
By default the click point is the glyph's centre. To anchor it elsewhere (e.g. a
tip-anchored pointer), pass hotspot in the glyph's own coords (its size /
SVG viewBox, origin top-left) โ the package centres the glyph in the auto-sized
bitmap and maps the hotspot in for you, so you never deal with box coordinates:
// A 32ร32 arrow whose tip is at (9, 3):
NativeMouseCursor.svg('pointer', 'assets/icons/pointer.svg',
hotspot: const Offset(9, 3));
๐ฅ๏ธ High-DPI & disposing #
Cursors bake at the DPR passed to configure and re-bake automatically when you
call configure again with a new one, so they stay crisp on Retina/HiDPI.
Release them when you're done:
NativeMouseCursor.dispose('rotate'); // ๐งน one cursor
NativeMouseCursor.disposeAll(); // ๐งผ everything
๐๏ธ Painted overlay (web / desktop) #
Want the cursor painted inside Flutter instead of as a real OS cursor? Wrap
your app in NativeMouseCursorOverlay(force: true): it hides the system cursor
and paints the same baked bitmap at the live pointer position.
MaterialApp(
builder: (context, child) =>
NativeMouseCursorOverlay(force: kIsWeb, child: child!),
home: const MyHomePage(),
);
This is useful where the system cursor can actually be hidden:
Web โ a perfectly seamless per-region cursor (the engine's CSS handling is best-effort across regions); the CSS cursor is hidden.
Android โ recommended for a rotating cursor: the native
PointerIconflickers when swapped rapidly (an OS quirk), so the painted overlay (system pointer hidden) gives smooth rotation.macOS / Windows / Linux โ preview the painted cursor (the native cursor is already pixel-perfect, so you rarely need this).
Off by default; the widget is a transparent pass-through unless force is set.
โ ๏ธ The overlay is a Flutter widget chasing the pointer, so it has a one-frame lag a real OS cursor doesn't. It only works where the system cursor can be hidden โ not on iOS/iPadOS (the system pointer can't be hidden, so a painted one would just double it).
๐งช Example #
The example/ app is an interactive showcase โ rotation (an arrow
that aims at a dot), mirroring (flipX/flipY), the hotspot (a red dot marking
the true pointer position), the baked shadow, and all four cursor sources โ plus
a switch to toggle the painted overlay.
cd example && flutter run -d macos # or -d chrome / windows / linux
โ๏ธ How it works #
NativeMouseCursor extends Flutter's MouseCursor. When the framework activates
the cursor for a pointer, the plugin asks the host to make the matching OS cursor
current (NSCursor.set() / SetCursor / gdk_window_set_cursor). Because
activation flows through Flutter's own cursor machinery, the OS cursor isn't
fought over by the engine's system-cursor handling. ๐ค
With NativeMouseCursorOverlay(force: true),
activation is intercepted instead: it keeps the baked bitmaps, hides the system
cursor, and paints the active cursor at the live pointer position.
๐ค Author #
Rami Al-Dhafiri.
๐ License #
MIT ยฉ Rami Al-Dhafiri.
