Skip to main content

Control a lightbox programmatically

Open, close, and navigate a lightbox from application code.

Every provider exposes one controller contract:

ts
interface LightboxController {
  readonly photos: ComputedRef<readonly PhotoItem[]>
  readonly count: ComputedRef<number>
  readonly activeIndex: ComputedRef<number>
  readonly activePhoto: ComputedRef<PhotoItem | null>
  readonly isOpen: ComputedRef<boolean>

  open(index?: number): Promise<void>
  openById(id: string): Promise<void>
  close(): Promise<void>
  next(): void
  prev(): void
  toggleZoom(): void
}

State is readonly. Invalid indexes and IDs reject with RangeError. Open and close promises settle after the latest lifecycle intent is reconciled.

PhotoGroup template ref

vue
<script setup lang="ts">
const group = ref<{
  open(index?: number): Promise<void>
  openById(id: string): Promise<void>
  close(): Promise<void>
}>()
</script>

<template>
  <PhotoGroup ref="group" :photos="photos">
    <PhotoAlbum :photos="photos" />
  </PhotoGroup>

  <button @click="group?.open(0)">Open first</button>
  <button @click="group?.openById('ocean')">Open ocean</button>
</template>

PhotoGroup refs expose open, openById, and close. Use a descendant useLightbox() call when you also need navigation and reactive state.

Controller inside a provider subtree

vue
<!-- GalleryToolbar.vue -->
<script setup lang="ts">
import { useLightbox } from '@lupinum/nuxt-photo/app'

const lightbox = useLightbox()
</script>

<template>
  <button @click="lightbox.prev()">Previous</button>
  <button @click="lightbox.next()">Next</button>
  <button @click="lightbox.close()">Close</button>
</template>

Render this component below <PhotoGroup> or <LightboxProvider>. Vue injection flows from parent to child, not within the same setup function.

app/components/GalleryDeepLink.vue
<script setup lang="ts">
import { useLightbox } from '@lupinum/nuxt-photo/app'

const lightbox = useLightbox()
const route = useRoute()
const router = useRouter()

onMounted(async () => {
  const id = route.query.photo
  if (typeof id !== 'string') return

  try {
    await lightbox.openById(id)
  } catch (error) {
    if (!(error instanceof RangeError)) throw error
    await router.replace({ query: { ...route.query, photo: undefined } })
  }
})

watch([lightbox.isOpen, lightbox.activePhoto], ([open, photo]) => {
  void router.replace({ query: { ...route.query, photo: open ? photo?.id : undefined } })
})
</script>

next() and prev() intentionally do nothing while closed. close() is idempotent. Provider-level configuration is setup-time; remount with a new key when it changes.