native_mouse_cursor 1.0.2 copy "native_mouse_cursor: ^1.0.2" to clipboard
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 logo]

native_mouse_cursor #

Turn any image, SVG, or painted glyph into a real OS mouse cursor β€” on Flutter desktop, web & Android.

pub package live demo license platforms

πŸš€ Try the live web demo β†’ (web preview β€” full native cursors shine on macOS / Windows / Linux / Android)

[native_mouse_cursor demo β€” a custom arrow cursor rotates to aim, mirrors across quadrants, and casts a baked drop shadow]

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 a SystemMouseCursors value 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? Call NativeMouseCursor.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 PointerIcon flickers 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.

1
likes
0
points
3.97k
downloads

Publisher

unverified uploader

Weekly Downloads

Turn any image, SVG, or painted glyph into a real OS mouse cursor on Flutter desktop, web & Android.

Repository (GitHub)
View/report issues

Topics

#cursor #mouse #pointer #ui #desktop

License

unknown (license)

Dependencies

flutter, flutter_svg, flutter_web_plugins, plugin_platform_interface, web

More

Packages that depend on native_mouse_cursor

Packages that implement native_mouse_cursor