Map Cursor
Coordinate cursor styling across map features with ownership-based priority.
Usage
import { useMapCursor, useMapCursorEffect } from '@accelint/map-toolkit/map-cursor';
import type { UniqueId } from '@accelint/core';
import type { PickingInfo } from '@deck.gl/core';
// Inside MapProvider - read and update cursor
function InteractiveLayer() {
const { cursor, requestCursorChange, clearCursor } = useMapCursor();
const handleHover = (info: PickingInfo) => {
if (info.object) {
requestCursorChange('pointer', 'layer-id');
} else {
clearCursor('layer-id');
}
};
return <ScatterplotLayer onHover={handleHover} />;
}
// Lifecycle-based cursor (auto-cleanup on unmount)
function DrawingMode() {
useMapCursorEffect('crosshair', 'drawing-mode');
return <div>Drawing active</div>;
}Reference
useMapCursor(id?: UniqueId): UseMapCursorReturn
Hook to access cursor state and actions. Uses useSyncExternalStore for concurrent-safe updates with a fan-out pattern.
Owner-based Priority System:
- Mode owners (from MapModeStore) have highest priority
- Non-owners can set cursors only in default/ownerless modes
- Rejected requests emit events with rejection reasons
Parameters
id(optional) - Map instance ID. If omitted, usesMapContext(requires being withinMapProvider).
Returns
type UseMapCursorReturn = {
cursor: CSSCursorType;
requestCursorChange: (cursor: CSSCursorType, requestOwner: string) => void;
clearCursor: (owner: string) => void;
};cursor- Current active cursor (CSS cursor string)requestCursorChange- Request cursor change with ownershipclearCursor- Clear cursor for specific owner
Throws
Error if no id is provided and hook is used outside MapProvider.
useMapCursorEffect(cursorType: CSSCursorType, owner: string, id?: UniqueId): void
Lifecycle hook that automatically manages cursor for a component.
Requests cursor on mount, maintains it while mounted, and clears on unmount. Does NOT re-render on cursor state changes—use useMapCursor if you need to read current cursor value.
Parameters
cursorType- The CSS cursor string to requestowner- Owner identifier for priority managementid(optional) - Map instance ID
Throws
Error if no id is provided and hook is used outside MapProvider.
Events
Cursor management uses event-driven coordination:
MapCursorEvents.changeRequest
Emitted when cursor change is requested.
type CursorChangeRequestPayload = {
cursor: CSSCursorType;
owner: string;
id: UniqueId;
};MapCursorEvents.changed
Emitted when cursor successfully changes.
type CursorChangedPayload = {
previousCursor: CSSCursorType;
currentCursor: CSSCursorType;
owner: string;
id: UniqueId;
};MapCursorEvents.rejected
Emitted when cursor request is rejected due to priority.
type CursorRejectionPayload = {
rejectedCursor: CSSCursorType;
rejectedOwner: string;
currentOwner: string;
reason: string;
id: UniqueId;
};Store Functions
// Get current cursor
getCursor(id: UniqueId): CSSCursorType;
// Clear state for cleanup
clearCursorState(id: UniqueId): void;
// Low-level store access
cursorStore.useSelector(id: UniqueId, selector: (state) => T): T;
cursorStore.actions(id: UniqueId): { requestCursorChange, clearCursor };Types
// CSS cursor string from React CSSProperties
type CSSCursorType = NonNullable<React.CSSProperties['cursor']>;Includes all standard CSS cursor keywords (pointer, crosshair, move, etc.) and custom url() syntax.
Examples
Example: Hover-based cursor change
import { useMapCursor } from '@accelint/map-toolkit/map-cursor';
import { ScatterplotLayer } from '@deck.gl/layers';
import type { PickingInfo } from '@deck.gl/core';
function PointLayer() {
const { requestCursorChange, clearCursor } = useMapCursor();
const handleHover = (info: PickingInfo) => {
if (info.object) {
// Show pointer when hovering over a point
requestCursorChange('pointer', 'point-layer');
} else {
// Clear cursor when not hovering
clearCursor('point-layer');
}
};
return (
<ScatterplotLayer
id="points"
data={points}
onHover={handleHover}
getPosition={(d) => d.position}
getRadius={10}
/>
);
}Example: Mode-coordinated cursor
import { useMapMode } from '@accelint/map-toolkit/map-mode';
import { useMapCursorEffect } from '@accelint/map-toolkit/map-cursor';
function DrawingMode() {
const { mode } = useMapMode();
// Automatically set crosshair cursor when in drawing mode
useMapCursorEffect(
mode === 'drawing' ? 'crosshair' : 'default',
'drawing-mode'
);
return mode === 'drawing' ? <DrawControls /> : null;
}Example: Custom cursor with icon
import { useMapCursor } from '@accelint/map-toolkit/map-cursor';
function CustomCursorControl() {
const { requestCursorChange } = useMapCursor();
const setCustomCursor = () => {
// Use custom cursor image
requestCursorChange(
'url(/cursors/crosshair.svg) 16 16, crosshair',
'custom-control'
);
};
return <button onClick={setCustomCursor}>Use Custom Cursor</button>;
}Example: Priority-aware cursor management
import { useMapMode } from '@accelint/map-toolkit/map-mode';
import { useMapCursor } from '@accelint/map-toolkit/map-cursor';
import { useEffect } from 'react';
function ModeAwareCursor() {
const { mode } = useMapMode();
const { requestCursorChange, clearCursor } = useMapCursor();
useEffect(() => {
// Mode owners have priority
if (mode === 'editing') {
requestCursorChange('move', 'editing-mode');
} else {
clearCursor('editing-mode');
}
}, [mode, requestCursorChange, clearCursor]);
return null;
}Example: Listening to cursor rejections
import { useOn } from '@accelint/bus/react';
import { MapCursorEvents } from '@accelint/map-toolkit/map-cursor';
import type { CursorRejectedEvent } from '@accelint/map-toolkit/map-cursor';
function CursorRejectionLogger({ mapId }: { mapId: UniqueId }) {
useOn<CursorRejectedEvent>(MapCursorEvents.rejected, (event) => {
if (event.payload.id === mapId) {
console.warn(
`Cursor request rejected: ${event.payload.reason}`,
`Owner: ${event.payload.rejectedOwner}`
);
}
});
return null;
}Example: Conditional cursor in layer
import { useMapCursor } from '@accelint/map-toolkit/map-cursor';
import { IconLayer } from '@deck.gl/layers';
import type { PickingInfo } from '@deck.gl/core';
function IconMarkerLayer({ editable }: { editable: boolean }) {
const { requestCursorChange, clearCursor } = useMapCursor();
const handleHover = (info: PickingInfo) => {
if (!editable) return;
if (info.object) {
requestCursorChange('grab', 'icon-layer');
} else {
clearCursor('icon-layer');
}
};
return (
<IconLayer
id="markers"
data={markers}
onHover={handleHover}
getPosition={(d) => d.position}
getIcon={() => 'marker'}
/>
);
}Good to know: Cursor changes respect map mode ownership. If a mode owner sets a cursor, non-owners' requests will be rejected until the mode changes.
Related
- BaseMap - Automatically creates MapProvider for cursor management
- useMapMode - Coordinate map modes with cursor ownership
- @accelint/bus - Event bus used for cursor coordination