React

usePicker, renderSlots, and two-way binding.

The adapter is a subpath of the same package. Nothing extra to install.

$ npm install loopem
Gallery.tsxtsx
import { usePicker } from 'loopem/react'

export function Gallery({ photos }) {
  const { containerRef, selectedItem, renderSlots } = usePicker({
    items: photos,
    layout: 'arc',
    radius: 900,
  })

  return (
    <>
      <img src={selectedItem?.full} alt="" />

      <div className="picker" ref={containerRef}>
        {renderSlots((photo) => (
          <button className="thumb">
            <img src={photo.thumb} alt="" />
          </button>
        ))}
      </div>
    </>
  )
}

renderSlots portals your nodes into the slots the picker owns and positions. Your components are ordinary React; loopem never renders their contents.

What you get back

containerRef · activeIndex · activeItem · selectedIndex · selectedItem · scrollTo · next · previous · select · seek · play · pause · renderSlots

Bind cheap things to activeItem and expensive ones to selectedItem. There is no update, because the items option drives it.

Two-way binding

Pass selectedIndex and onSelect and the picker is controlled, exactly the way value and onChange control an <input>:

tsx
const [chosen, setChosen] = useState(0)

const { containerRef, renderSlots } = usePicker({
  items: dates,
  selectedIndex: chosen,
  onSelect: setChosen,
})

selectedIndex decides where the track starts and the track follows it on every change, so setting state from a link, a saved booking or a server response moves the picker. Omit it and the picker owns its own selection from initialIndex.

Notes

  • containerRef is typed structurally, not as React’s RefObject, because that is spelled differently across major versions. It is assignable to ref in 18 and 19 alike.
  • Preact needs nothing: loopem/react works under preact/compat.
  • Working code: the React gallery and the date picker.