Make any Sprite2D, AnimatedSprite2D, or TileMap2D respond to clicks and hovers. They implement three.js’s raycast(), so picking works the standard way — pointer-event props in React Three Fiber, a plain Raycaster in vanilla three.js. This guide is the task recipes; for a full runnable walkthrough see the Hit Testing example.
Make a sprite respond to the pointer
Section titled “Make a sprite respond to the pointer”In R3F the events are props. In vanilla three.js you load a Raycaster from the pointer position and read back the first hit.
<sprite2D texture={texture} scale={[48, 48, 1]} onPointerOver={(e) => { e.stopPropagation(); setHovered(true) }} onPointerOut={() => setHovered(false)} onClick={(e) => { e.stopPropagation(); collect() }}/>import { Raycaster, Vector2 } from 'three'
const raycaster = new Raycaster()const pointer = new Vector2()
renderer.domElement.addEventListener('pointerdown', (e) => { const rect = renderer.domElement.getBoundingClientRect() pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1 pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1 raycaster.setFromCamera(pointer, camera)
const hit = raycaster.intersectObjects(pickableSprites, false)[0] if (hit) collect(hit.object)})Keep the vanilla pickableSprites array in sync with what should be interactive — drop a sprite from it when it is collected so it stops receiving hits.
Pointer events in vanilla three.js
Section titled “Pointer events in vanilla three.js”The R3F path above gives you bubbling, e.stopPropagation(), e.point (the world-space hit), and pointer capture for free. Vanilla three.js ships no event system, so you have two ways to get those semantics:
- Roll your own — the
Raycasterloop above. Fine for a handful of pickable objects, and you keep full control of when the cast runs. - Use
@pmndrs/pointer-events— the framework-agnostic event system that backs R3F. ItsforwardHtmlEventstranslates DOM pointer events into raycasts each frame and dispatchespointerover/pointerout/clickwith capture and bubbling. Flatland sprites participate unchanged because they implementraycast(), the same contract R3F relies on.
Reach for the raw Raycaster when you want control over the cast; reach for @pmndrs/pointer-events when you want the events handed to you.
Choose a hit-test mode
Section titled “Choose a hit-test mode”Every sprite carries a hitTestMode that controls what the ray tests against. Pick the one that matches the shape you are picking:
'radius'— the inscribed circle of the quad. The default; right for round or square icons, and it avoids false corner hits on small sprites.'bounds'— the full quad’s bounding box. Right for rectangular sprites with no wasted margin.'alpha'— opaque pixels only, for irregular shapes. See the next section.'none'— rendered but never picked; the ray skips the object entirely.
sprite.hitTestMode = 'bounds'ground.hitTestMode = 'none' // visible, but clicks pass straight throughThe full table with cost and memory trade-offs is in the Hit Testing example, and every property is in the Sprite2D API reference.
Pick by opaque pixels (alpha mode)
Section titled “Pick by opaque pixels (alpha mode)”For characters, foliage, and large sprites with transparent cutouts, 'alpha' rejects rays that land on fully transparent pixels. It does a bounds pre-check first, then samples a CPU-side alpha map only when the box is hit. Three steps:
- Bake a sidecar at build time:
flatland-bake alpha sprites.pngwritessprites.alpha.png(see the Baking guide). - Load it by passing
alpha: truetoSpriteSheetLoader; the result lands onsheet.alphaMap. - Enable it: assign
sprite.alphaMap = sheet.alphaMapand setsprite.hitTestMode = 'alpha'.AnimatedSprite2Dadoptssheet.alphaMapfrom itsspriteSheetautomatically, so you only set the mode.
const sheet = await SpriteSheetLoader.load('/sprites/sprites.json', { alpha: true })
sprite.alphaMap = sheet.alphaMapsprite.hitTestMode = 'alpha'sprite.alphaThreshold = 0.5 // pixels below this alpha are transparent (default 0.5)The sidecar is optional: without it the loader falls back to a one-time canvas readback of the loaded texture and warns in development. Don’t reach for alpha on small sprites like coins — 'radius' is cheaper and the difference is invisible at that size. The example walks the full bake-and-load flow.
Pick a tile from a tilemap
Section titled “Pick a tile from a tilemap”A ray that hits a TileMap2D gives you back the cell with tileFromIntersection:
const hits = raycaster.intersectObject(tilemap, true)if (hits.length > 0) { const cell = tilemap.tileFromIntersection(hits[0]) // cell → { layer: number, tileX: number, tileY: number, gid: number }}Keep hover correct under a moving camera
Section titled “Keep hover correct under a moving camera”When you pan or zoom the camera while the pointer is still, R3F does not re-fire the last pointer event, so a sprite sliding under a stationary cursor won’t get onPointerOver. Force a re-evaluation each frame from useFrame:
useFrame((state) => { // advance camera, pan, zoom… state.events.update()})Batched sprites, hierarchy, and clipping
Section titled “Batched sprites, hierarchy, and clipping”Batched sprites use the same per-sprite hit-test modes. SpriteGroup keeps a spatial index for each batch, then returns intersections against the original Sprite2D, so event handlers and hit.object do not change when a sprite becomes batched.
Picking follows the same hierarchy as rendering: ancestor transforms affect the hit location, hidden ancestors reject hits, and a SpriteGroup.clipRect rejects hits outside its transformed local rectangle.
Known limitation
Section titled “Known limitation”Alpha sampling for rotated or trimmed atlas frames does not yet apply the frame’s rotation and trim offset. Rendering is correct, but hitTestMode: 'alpha' can sample the wrong atlas pixel for those frames. Prefer 'bounds' for affected sprites until atlas-aware alpha sampling lands.
Further out, each building on this same contract: a pointer-driven render-to-texture portal (#126), a GPU ID-buffer path for very high instance counts (#128), an ergonomic drag helper composed from these pointer events (#129), path-accurate Skia node picking (#130), and meta.alpha atlas-schema discovery for the sidecar (#124).
Next steps
Section titled “Next steps”- Hit Testing example — the full runnable walkthrough, dual-framework, with the complete modes table and alpha bake.
- Sprites —
Sprite2Dproperties, anchors, and layers. - Loaders — spritesheet format and the alpha sidecar option.
- Baking — the
flatland-bakeCLI and sidecar files. Sprite2DAPI reference — every hit-test property and option.