← All @molecule/* packages · App templates

@molecule/app-annotation-pin-react

Feature · 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

npm · Source on GitHub

How it works

@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

Reference

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.ts JSDoc, 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.

Quick Start

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>
  )
}

Type

feature

Installation

npm install @molecule/app-annotation-pin-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react

API

Interfaces

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
}

Pin

A 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
}

Functions

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:

  • clicking a pin marker fires onPinClick(pinId)
  • clicking empty space on the layer fires onSurfaceClick({ x, y }), which callers typically use to add a new pin

The 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.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-react ^1.0.1
  • @molecule/app-ui ^1.0.1
  • react ^18.0.0 || ^19.0.0

Runtime Dependencies

  • @molecule/app-react
  • @molecule/app-ui
  • react

Selection 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.

Translations

Translation strings are provided by @molecule/app-locales-annotation-pin.