← All @molecule/* packages · App templates
@molecule/app-annotation-pin-reactFeature · annotation-pin · App (browser) · v1.0.1 · Apache-2.0
Pin/annotation marker overlay with click-to-place pins and label/note popups for 3d-model-viewer, image annotations, map pins, hotspot tours
npm install @molecule/app-annotation-pin-react@molecule/app-annotation-pin-react is a ready-made annotation-pin feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
import { AnnotationLayer, type Pin } from '@molecule/app-annotation-pin-react'
function ImageAnnotator({ src, pins, setPins, activeId, setActiveId }) {
return (
<AnnotationLayer
pins={pins}
activePinId={activeId}
onPinClick={(id) => setActiveId(id === activeId ? null : id)}
onSurfaceClick={({ x, y }) => {
const id = crypto.randomUUID()
setPins([...pins, { id, position: { x, y }, label: pins.length + 1 }])
setActiveId(id)
}}
>
<img src={src} alt="" style={{ width: '100%', display: 'block' }} />
</AnnotationLayer>
)
}Works with: @molecule/app-react, @molecule/app-ui
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Annotation pin / hotspot overlay primitives.
Exports <AnnotationPin> (single click-to-toggle marker) and
<AnnotationLayer> (parent wrapper that manages many pins on a
surface). Reusable for 3d-model viewers, image annotations, map
pins, and hotspot tours.
import { AnnotationLayer, type Pin } from '@molecule/app-annotation-pin-react'
function ImageAnnotator({ src, pins, setPins, activeId, setActiveId }) {
return (
<AnnotationLayer
pins={pins}
activePinId={activeId}
onPinClick={(id) => setActiveId(id === activeId ? null : id)}
onSurfaceClick={({ x, y }) => {
const id = crypto.randomUUID()
setPins([...pins, { id, position: { x, y }, label: pins.length + 1 }])
setActiveId(id)
}}
>
<img src={src} alt="" style={{ width: '100%', display: 'block' }} />
</AnnotationLayer>
)
}
feature
npm install @molecule/app-annotation-pin-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react
AnnotationLayerProps<AnnotationLayer> props.
interface AnnotationLayerProps {
/** Pins to render on the layer. */
pins: Pin[]
/** Optional layer content (the surface to annotate — image, canvas, map tile, etc.). */
children?: ReactNode
/**
* The currently active (popup-open) pin id, or `null` for none. The
* layer is fully controlled — callers manage selection state.
*/
activePinId?: string | null
/** Called with the clicked pin id when a pin marker is clicked. */
onPinClick?: (pinId: string) => void
/**
* Called when the user clicks an empty area of the layer (i.e. not on
* a pin marker or a popup). Coordinates are in the same space as
* `Pin.position` — normalised 0..1 by default.
*/
onSurfaceClick?: (position: { x: number; y: number }) => void
/**
* Whether positions are normalised (0..1) — when `true` the layer
* computes click coordinates as fractions of its bounding box; when
* `false` it returns raw pixel offsets. Defaults to `true`.
*/
normalised?: boolean
/**
* Side the popup renders on, relative to its marker. Defaults to `'right'`.
*/
popupSide?: 'top' | 'bottom' | 'left' | 'right'
/** Extra classes merged onto the layer root. */
className?: string
/** Inline style for the layer root. */
style?: CSSProperties
}
AnnotationPinProps<AnnotationPin> props.
interface AnnotationPinProps {
/** Position in the parent surface's coordinate space (typically normalised 0..1). */
position: { x: number; y: number }
/** Short label (e.g. an index or single-word tag) shown inside the marker. */
label?: ReactNode
/** Longer note shown inside the popup when the pin is selected. */
note?: ReactNode
/**
* Whether the pin is currently selected. When `true` the popup is
* rendered. Selection is controlled — callers manage state in
* `<AnnotationLayer>` (or themselves) and update via `onClick`.
*/
selected?: boolean
/** Click handler — fires when the pin marker is clicked. */
onClick?: () => void
/**
* Side the popup renders on, relative to the marker. Defaults to `'right'`.
*/
popupSide?: 'top' | 'bottom' | 'left' | 'right'
/** Extra classes merged onto the root wrapper. */
className?: string
/**
* Whether `position` is normalised (0..1) — when `true` the wrapper
* positions itself with `%` units so pins re-anchor on resize. When
* `false` the wrapper uses raw `px` values. Defaults to `true`.
*/
normalised?: boolean
}
PinA single pin to display on a parent surface. position is in the
parent's coordinate space — for image / 2D-canvas overlays this is
normalised 0..1 (so the pin re-anchors as the surface resizes); for
pixel-positioned overlays callers can pass raw px values and provide
their own style overrides via className.
The label/note are pure data — components handle render only. Surfaces
supply pin objects via <AnnotationLayer pins={...}> (or render a
single pin directly if there is only one).
interface Pin {
/** Stable identifier (used as React key). */
id: string
/** Position in the parent surface's coordinate space (typically normalised 0..1). */
position: { x: number; y: number }
/** Short label (e.g. an index or single-word tag) shown inside the pin marker. */
label?: ReactNode
/** Longer note / description body, shown inside the popup when the pin is selected. */
note?: ReactNode
}
AnnotationLayer(props)Wrapper that manages multiple pins on a parent surface. Renders its
children (the surface — typically an <img>, <canvas>, or
map-tile element) and overlays each pins[] entry as a clickable
<AnnotationPin> marker.
Click behaviour:
onPinClick(pinId)onSurfaceClick({ x, y }),
which callers typically use to add a new pinThe layer itself sets position: relative so absolute pin
positioning works without the caller having to provide a wrapper.
All styling routes through getClassMap() (no Tailwind / raw class
names). All user-visible text routes through t() so the layer
translates via the companion
@molecule/app-locales-annotation-pin locale bond.
function AnnotationLayer(props: AnnotationLayerProps): JSX.Element
props — Component props.Returns: The annotation-layer element.
AnnotationPin(props)A single pin marker (small circle with optional label) rendered at
an absolute position on its parent. Clicking the marker toggles its
popup (which shows the note body) via the onClick handler.
Positioning is absolute relative to the nearest positioned ancestor —
callers must wrap the surface (image, canvas, map) in a
position: relative element. <AnnotationLayer> provides this
wrapper automatically.
All styling routes through getClassMap() (no Tailwind / raw class
names). All user-visible text (aria-labels, fallback note text)
routes through t() so the marker translates via the companion
@molecule/app-locales-annotation-pin locale bond.
function AnnotationPin(props: AnnotationPinProps): JSX.Element
props — Component props.Returns: The pin element.
Peer dependencies:
@molecule/app-react ^1.0.1@molecule/app-ui ^1.0.1react ^18.0.0 || ^19.0.0@molecule/app-react@molecule/app-uireactSelection is fully controlled — the layer never stores the active pin;
manage activePinId yourself and toggle it in onPinClick. position
is normalised 0..1 by default (fractions of the layer box, so pins
re-anchor on resize); pass normalised={false} for raw pixel offsets —
on BOTH <AnnotationLayer> and any directly-rendered <AnnotationPin>,
or clicks and markers will disagree. The layer wraps children in a
position: relative box; a bare <AnnotationPin> needs its own
positioned ancestor. Translations come from the companion
@molecule/app-locales-annotation-pin locale bond.
Translation strings are provided by @molecule/app-locales-annotation-pin.