Vue Lightbox

Lightbox de galería de imágenes y vídeos a pantalla completa para Vue 3, con @reelkit/vue-lightbox.

Ver la demo en vivo →

Características

Imágenes y vídeo
Slides de vídeo incluidos
Gestos táctiles
Desliza para navegar
Deslizar para cerrar
Desliza hacia arriba para cerrar
Teclado
Flechas + Escape
Pantalla completa
API compatible con todos los navegadores
Transiciones
Deslizar, fundido, volteo, zoom
Precarga
Se cargan antes los 2 vecinos de cada lado
Botón de sonido
Silencio por slide
Estados de carga
Spinner y slot propio
Gestión de errores
Icono de error y slot propio
Scoped slots
6 zonas de slots personalizables
v-model
Enlace bidireccional con v-model:is-open
Estado en la URL
Enlaces que se pueden compartir y guardar

Instalación

bash
npm install @reelkit/vue-lightbox @reelkit/vue lucide-vue-next

No olvides importar los estilos:

typescript
import '@reelkit/vue-lightbox/styles.css';
Iconos

Los controles por defecto usan lucide-vue-next para los iconos. Si prefieres otra librería de iconos, usa los scoped slots #controls y #navigation para poner los tuyos.

Uso básico

Importa la hoja de estilos y el componente LightboxOverlay, y controla la apertura y el cierre con v-model:is-open.

App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { LightboxOverlay, type LightboxItem } from '@reelkit/vue-lightbox';
import '@reelkit/vue-lightbox/styles.css';

const images: LightboxItem[] = [
  {
    src: '/cdn/samples/images/image-01.jpg',
    title: 'Mountain River',
    description: 'A beautiful mountain river',
  },
  {
    src: '/cdn/samples/images/image-02.jpg',
    title: 'Snowy Peaks',
  },
  {
    src: '/cdn/samples/images/image-03.jpg',
    title: 'Misty Forest',
    description: 'Morning fog over the forest canopy',
  },
  {
    src: '/cdn/samples/images/image-04.jpg',
    title: 'Autumn Trail',
  },
  {
    src: '/cdn/samples/images/image-05.jpg',
    title: 'Ocean Cliff',
    description: 'Dramatic cliffs above the Pacific',
  },
  {
    src: '/cdn/samples/images/image-06.jpg',
    title: 'Desert Dunes',
  },
];

const open = ref(false);
const startIndex = ref(0);

function openAt(i: number) {
  startIndex.value = i;
  open.value = true;
}
</script>

<template>
  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">
    <button
      v-for="(img, i) in images"
      :key="img.src"
      style="aspect-ratio:4/3;cursor:pointer"
      @click="openAt(i)"
    >
      <img :src="img.src" style="width:100%;height:100%;object-fit:cover" />
    </button>
  </div>

  <LightboxOverlay
    v-model:is-open="open"
    :items="images"
    :initial-index="startIndex"
  />
</template>

Scoped slots

Seis scoped slots con nombre permiten personalizar por completo las superficies del overlay. Omite el slot para conservar el valor por defecto incluido; si no pones nada dentro del slot (por ejemplo, con v-if="false"), esa sección se oculta del todo.

SlotÁmbitoDescripción
#slideSlideSlotScopeSustituye el contenido de cada slide (necesario para los slides de vídeo)
#controlsControlsSlotScopeSustituye la barra de controles superior (cerrar, contador, pantalla completa)
#navigationNavigationSlotScopeSustituye las flechas de anterior y siguiente
#infoInfoSlotScopeSustituye el overlay inferior con degradado del título y la descripción
#loadingLoadingSlotScopeIndicador de carga propio
#errorErrorSlotScopeIndicador de error propio
vue
<template>
  <LightboxOverlay v-model:is-open="open" :items="items">
    <!-- Custom info overlay -->
    <template #info="{ item }">
      <div class="my-caption">
        <h2>{{ item.title }}</h2>
        <p>{{ item.description }}</p>
      </div>
    </template>

    <!-- Custom navigation -->
    <template #navigation="{ onPrev, onNext, activeIndex, count }">
      <div class="my-nav">
        <button :disabled="activeIndex === 0" @click="onPrev">Prev</button>
        <span>{{ activeIndex + 1 }} / {{ count }}</span>
        <button :disabled="activeIndex === count - 1" @click="onNext">Next</button>
      </div>
    </template>

    <!-- Custom controls -->
    <template #controls="{ onClose, isFullscreen, onToggleFullscreen }">
      <div class="my-controls">
        <button @click="onToggleFullscreen">
          {{ isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen' }}
        </button>
        <button @click="onClose">Close</button>
      </div>
    </template>
  </LightboxOverlay>
</template>

Compatibilidad con vídeo

Los slides de vídeo son opcionales para que el bundle por defecto no incluya nada de audio ni vídeo. Llama a useVideoSlideRenderer(items) y pasa el VideoSlideRenderer / VideoControlsRenderer que devuelve a los slots #slide y #controls del overlay. Envuelve el overlay con el SoundProvider devuelto para que el botón de sonido incluido tenga contexto.

vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  LightboxOverlay,
  useVideoSlideRenderer,
  type LightboxItem,
} from '@reelkit/vue-lightbox';
import '@reelkit/vue-lightbox/styles.css';

const open = ref(false);
const items: LightboxItem[] = [
  { src: '/image-01.jpg', title: 'Image' },
  {
    type: 'video',
    src: '/clip.mp4',
    poster: '/clip.jpg',
    title: 'Clip',
  },
];

const { VideoSlideRenderer, VideoControlsRenderer, SoundProvider } =
  useVideoSlideRenderer(items);
</script>

<template>
  <SoundProvider>
    <LightboxOverlay v-model:is-open="open" :items="items">
      <template #slide="scope">
        <VideoSlideRenderer v-bind="scope" />
      </template>
      <template #controls="scope">
        <VideoControlsRenderer v-bind="scope" />
      </template>
    </LightboxOverlay>
  </SoundProvider>
</template>

El elemento <video> compartido que usan los slides de vídeo sigue el mismo patrón que el reel-player de Vue: en iOS la reproducción continúa entre cambios de slide sin necesitar un gesto del usuario en cada uno.

Pantalla completa

Usa useFullscreen de @reelkit/vue para observar o cambiar el estado de pantalla completa de un elemento referenciado. El lightbox controla su botón de pantalla completa incluido con el mismo composable.

vue
<script setup lang="ts">
import { shallowRef } from 'vue';
import { useFullscreen } from '@reelkit/vue';

const containerRef = shallowRef<HTMLDivElement | null>(null);
const { isFullscreen, toggle } = useFullscreen({ elementRef: containerRef });
</script>

<template>
  <div ref="containerRef">
    <button @click="toggle">
      {{ isFullscreen.value ? 'Exit fullscreen' : 'Enter fullscreen' }}
    </button>
  </div>
</template>

Estado en la URL

Ver la demo en vivo →

Crea un controlador con useOverlayUrlState de @reelkit/vue y pásalo a LightboxUrlOverlay como controller, y la barra de direcciones controlará la galería: se abre sola cuando el parámetro nombra un slide y se cierra cuando el parámetro desaparece. Los enlaces se pueden compartir y el botón de volver cierra la galería. Es un componente distinto de LightboxOverlay, así que cada uno tiene una única forma de controlar el estado abierto: el modelo is-open o el controller de la URL, nunca los dos.

Claves incluidas

Puedes apuntar a los slides con una clave incluida: pasa con spread urlIndexKey (por posición) o urlStableIdKey (por un id estable) al controlador; las dos se reexportan desde @reelkit/vue. Consulta la guía del estado en la URL y la API del core.

Volver solo cierra cuando abriste la galería desde dentro de la aplicación: el enlace añadió una entrada, así que volver regresa a la galería. Un enlace compartido abierto directamente en una pestaña nueva no tiene historial detrás, así que el botón de volver del navegador sale del sitio; el botón de cerrar o Escape quitan el parámetro en el sitio y te dejan en la galería.

vue
<script setup lang="ts">
import { LightboxUrlOverlay, type LightboxItem } from '@reelkit/vue-lightbox';
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/vue';
import '@reelkit/vue-lightbox/styles.css';

const props = defineProps<{ images: LightboxItem[] }>();

const photo = useOverlayUrlState({
  param: 'photo',
  ...urlIndexKey(() => props.images.length),
});
</script>

<template>
  <!-- Opening is a link — the href is the open action. No open flag, no
       handler: the overlay reads the URL and opens itself. -->
  <RouterLink v-for="(img, i) in props.images" :key="img.src" :to="`?photo=${i}`">
    <img :src="img.src" />
  </RouterLink>

  <LightboxUrlOverlay :controller="photo" :items="props.images" />
</template>

El composable recibe un objeto de opciones y devuelve un UrlStateController (con set, index y value). Guárdalo para controlarlo por código: set es la escritura de bajo nivel que usa el overlay internamente (al cambiar de slide y con set(null) para cerrar). También maneja el overlay por código: set(index) lo abre, igual que navegar al parámetro. Aun así, es mejor abrirlo con un enlace: el href se puede compartir, se abre en una pestaña nueva y el botón de volver lo cierra, todo gratis y sin manejador.

Las opciones completas de useOverlayUrlState (param, adapter, codec, locator) están en la referencia de la API de Vue.

LightboxUrlOverlay solo recibe :controller (obligatorio), un emit @close y todas las props visuales y de comportamiento que reenvía LightboxOverlay (items, transition-fn, los scoped slots y demás), pero no is-open.

  • Abrir cuesta una entrada en el historial; pasar slides la sustituye, así que cien deslizamientos no añaden ninguna y volver una vez siempre sale de la galería.
  • Un enlace compartido como ?photo=3 abre la galería en ese slide. Un parámetro que no nombra ningún slide se quita de la URL en lugar de apuntar a un slide que no se puede abrir.

En una aplicación con router, pasa un adaptador. Escribir directamente en el historial deja desfasada la ubicación del propio router, y su siguiente navegación pierde el parámetro.

vue
<script setup lang="ts">
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';

const adapter = useVueRouterUrlAdapter();
const photo = useOverlayUrlState({
  param: 'photo',
  adapter,
  ...urlIndexKey(() => images.length),
});
</script>

<template>
  <LightboxUrlOverlay :controller="photo" :items="images" />
</template>

Enlaces estables. El índice es posicional, así que un marcador abre otra imagen en cuanto la lista se reordena. urlStableIdKey usa como clave el id estable de cada elemento y recorre la lista actual; una sola llamada cubre el caso habitual.

vue
<script setup lang="ts">
const photo = useOverlayUrlState({
  param: 'photo',
  ...urlStableIdKey({ items: () => images }),
});
</script>

<template>
  <LightboxUrlOverlay :controller="photo" :items="images" />
</template>

Pasa hashCodec: base64UrlCodec para codificar el id en base64url en la URL: es una ofuscación reversible, no un hash criptográfico.

Si usas otro campo como clave (un slug) o paginas un feed infinito con locateAsync, crea tú el codec y el locator: codec escribe la identidad en la URL y locator encuentra dónde está ahora.

vue
<script setup lang="ts">
const photo = useOverlayUrlState({
  param: 'photo',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => images.findIndex((x) => x.slug === id),
    identify: (index) => images[index].slug,
  },
});
</script>

<template>
  <LightboxUrlOverlay :controller="photo" :items="images" />
</template>

Galerías infinitas o paginadas. locate es síncrono, así que solo puede responder por los elementos ya cargados: un enlace compartido a la imagen 400 de un feed que ha cargado 20 no encuentra nada. locateAsync es la alternativa y solo se llama cuando falla: carga las páginas que necesites y devuelve el índice que resulte tener la identidad.

Atajo

¿Usas el id del elemento como clave? Sáltate el codec y el locator hechos a mano y pasa locateAsync directamente a urlStableIdKey({ items, locateAsync }) (carga cuando no lo encuentra y después devuelve el índice). La versión más completa de abajo es para usar otro campo como clave o para tener el control total.

vue
<script setup lang="ts">
const photo = useOverlayUrlState({
  param: 'photo',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => items.value.findIndex((x) => x.id === id),
    identify: (index) => items.value[index].id,
    locateAsync: async (id) => {
      const loaded = await loadById(id); // or loadUntil(id) — fetch just that one, or page up to it
      if (!loaded) return null; // exhausted — link names no item
      items.value = loaded; // commit; the overlay renders from this
      return loaded.findIndex((x) => x.id === id); // wherever it landed
    },
  },
});
</script>

<template>
  <LightboxUrlOverlay :controller="photo" :items="items" />
</template>

Mientras está pendiente, el lightbox sigue cerrado y el parámetro no se toca, así que el enlace directo sobrevive a la carga. null o un rechazo quitan el parámetro. Una respuesta que llega después de que la URL haya cambiado, después de cerrar o después de desmontar se descarta, así que una carga lenta no puede abrir un slide que nadie ha pedido. Lo que devuelva manda: indica el índice de los datos que acaba de cargar, y el lightbox lo usa tal cual en lugar de volver a leer items, que Vue todavía no ha vuelto a renderizar.

Referencia de la API

Props de LightboxOverlay

LightboxOverlayProps

PropTipoPor defectoDescripción
isOpenbooleanobligatorioControla la visibilidad; con false el overlay se quita del DOM. Se puede enlazar con v-model:is-open.
itemsLightboxItem[]obligatorioArray de elementos (imágenes o vídeos)
initialIndexnumber0Índice, empezando en cero, del elemento visible al principio
transitionFnTransitionTransformFnslideTransitionFunción de transición entre slides. Importa una incluida (slideTransition, flipTransition, lightboxFadeTransition, lightboxZoomTransition) o pasa una propia. Si se omite, se usa slideTransition.
showInfobooleantrueIndica si se renderiza el overlay de información con el título y la descripción
showControlsbooleantrueIndica si se renderiza la barra de controles superior (cerrar, contador, pantalla completa)
showNavigationbooleantrueIndica si se renderizan las flechas de anterior y siguiente (solo en escritorio)
transitionDurationnumber300Duración de la animación de los slides en ms
swipeDistanceFactornumber0.12Fracción mínima de la distancia de deslizamiento (0–1) para cambiar de slide
swipeToCloseDirection'up' | 'down''up'Dirección del gesto de deslizar para cerrar en móvil
loopbooleanfalseIndica si el slider pasa del último slide al primero
enableNavKeysbooleantrueActiva la navegación con las teclas de flecha
enableWheelbooleantrueActiva la navegación con la rueda del ratón
wheelDebounceMsnumber200Duración del debounce de los eventos de la rueda en ms
ariaLabelstring'Image gallery'Etiqueta accesible de la región del diálogo

Props de LightboxUrlOverlay

LightboxUrlOverlayProps

Acepta todas las props visuales y de comportamiento de arriba salvo is-open, que se sustituye por controller. Emite close, slide-change y api-ready, pero no update:is-open. initial-index se ignora aquí: la posición del controlador elige el slide, así que un valor pasado junto a él se sobrescribiría en cada apertura.

PropTipoPor defectoDescripción
controllerUrlStateControllerobligatorioControlador de useOverlayUrlState. Su posición decide si el overlay está abierto y qué slide muestra; el overlay escribe a través de él al cambiar de slide y al cerrar.

Eventos de LightboxOverlay

EventoDatosDescripción
closevoidSe emite cuando el usuario cierra el lightbox
slide-changenumberSe emite con el índice del nuevo slide activo después de un cambio
api-readyLightboxApiSe emite cuando el slider está listo y expone la API imperativa
update:is-openbooleanSe emite al cerrar; permite usar v-model:is-open

Interfaz LightboxItem

CampoTipoObligatorioDescripción
srcstringURL de la imagen o el vídeo
type'image' | 'video'noTipo de elemento. Por defecto es 'image'
posterstringnoImagen en miniatura de los vídeos
titlestringnoTítulo que se muestra en el overlay de información
descriptionstringnoDescripción que se muestra debajo del título
widthnumbernoAncho intrínseco de la imagen en píxeles
heightnumbernoAlto intrínseco de la imagen en píxeles

Tipos del ámbito de slot

TipoCampos
SlideSlotScope{ item, index, size: [number, number], isActive, onReady, onWaiting, onError }
ControlsSlotScope{ item, activeIndex, count, isFullscreen, onClose, onToggleFullscreen }
NavigationSlotScope{ item, activeIndex, count, onPrev, onNext }
InfoSlotScope{ item, index }
LoadingSlotScope{ item, activeIndex }
ErrorSlotScope{ item, activeIndex }

Transiciones

Pasa cualquier TransitionTransformFn con la prop transition-fn. Si importas solo la transición que usas, el bundler puede descartar el resto con tree-shaking. Si se omite, se usa slideTransition.

FunciónDescripción
slideTransitionPor defecto. Desplazamiento horizontal entre slides; se reexporta desde @reelkit/vue.
lightboxFadeTransitionFundido cruzado con un ligero desplazamiento horizontal. Propia de @reelkit/vue-lightbox.
flipTransitionVolteo 3D sobre el eje Y; se reexporta desde @reelkit/vue.
lightboxZoomTransitionEl slide entrante escala del 70 % al 100 % con fundido. Propia de @reelkit/vue-lightbox.
vue
<script setup lang="ts">
import {
  LightboxOverlay,
  lightboxFadeTransition,
  lightboxZoomTransition,
} from '@reelkit/vue-lightbox';
</script>

<template>
  <!-- Built-in transition -->
  <LightboxOverlay
    v-model:is-open="open"
    :items="items"
    :transition-fn="lightboxFadeTransition"
  />

  <!-- Different built-in -->
  <LightboxOverlay
    v-model:is-open="open"
    :items="items"
    :transition-fn="lightboxZoomTransition"
  />
</template>

Carga de contenido y gestión de errores

Cuando te encargas del renderizado con el slot #slide, el ámbito del slot ofrece tres callbacks del ciclo de vida para indicar el estado de carga. El lightbox sigue el estado de cada slide y muestra un spinner o un icono de error según corresponda. Un precargador de contenido guarda en caché las URLs rotas, así que al volver a un slide que falló no se reintenta.

Callbacks del ciclo de vida

CallbackTipoDescripción
onReady() => voidIndica que el contenido del slide se ha cargado bien (por ejemplo, la imagen se ha decodificado)
onWaiting() => voidIndica que el contenido del slide se está cargando (muestra el spinner)
onError() => voidIndica que el contenido del slide no se ha podido cargar (muestra el icono de error)

Conectar los callbacks en #slide

vue
<template>
  <LightboxOverlay v-model:is-open="open" :items="items">
    <template
      #slide="{ item, size, isActive, onReady, onWaiting, onError }"
    >
      <template v-if="item.type === 'video'">
        <video
          :src="item.src"
          :poster="item.poster"
          :autoplay="isActive"
          :style="{ width: `${size[0]}px`, height: `${size[1]}px`, objectFit: 'contain' }"
          @canplay="onReady"
          @waiting="onWaiting"
          @error="onError"
        />
      </template>
      <template v-else>
        <img
          :src="item.src"
          :style="{ width: `${size[0]}px`, height: `${size[1]}px`, objectFit: 'contain' }"
          @load="onReady"
          @error="onError"
        />
      </template>
    </template>
  </LightboxOverlay>
</template>

Slot de carga propio

Usa el slot #loading para sustituir el spinner por defecto.

vue
<template>
  <LightboxOverlay v-model:is-open="open" :items="items">
    <template #loading="{ item, activeIndex }">
      <div class="my-loading">
        <span>Loading image {{ activeIndex + 1 }}…</span>
        <span class="muted">{{ item.title }}</span>
      </div>
    </template>
  </LightboxOverlay>
</template>

Slot de error propio

Usa el slot #error para sustituir el icono de imagen rota por defecto.

vue
<template>
  <LightboxOverlay v-model:is-open="open" :items="items">
    <template #error="{ item, activeIndex }">
      <div class="my-error">
        <span>Failed to load</span>
        <span class="muted">{{ item.title ?? item.src }}</span>
      </div>
    </template>
  </LightboxOverlay>
</template>

Clases CSS

Todas las clases CSS son normales (no scoped), así que puedes apuntar a ellas con selectores más específicos en una hoja de estilos cargada después de @reelkit/vue-lightbox/styles.css. Para cambiar colores, tamaños y z-index, es mejor usar las propiedades personalizadas de CSS de la sección Temas de más abajo.

ClaseComponenteDescripción
.rk-lightbox-overlayOverlayContenedor raíz (fondo a pantalla completa)
.rk-lightbox-top-shadeOverlayDegradado superior detrás de los controles
.rk-lightbox-spinnerOverlaySpinner de carga por defecto
.rk-lightbox-errorOverlayContenedor del estado de error (imagen rota)
.rk-lightbox-error-textOverlayTexto del estado de error
.rk-lightbox-controls-leftControlsContenedor de los controles de arriba a la izquierda
.rk-lightbox-btnControlsBotón de control (pantalla completa, sonido, etc.)
.rk-lightbox-closeControlsBotón de cerrar
.rk-lightbox-counterControlsChip del contador de imágenes
.rk-lightbox-navNavigationFlecha de navegación (anterior y siguiente)
.rk-lightbox-nav-prevNavigationFlecha de anterior
.rk-lightbox-nav-nextNavigationFlecha de siguiente
.rk-lightbox-infoInfoContenedor del título y la descripción
.rk-lightbox-info-titleInfoTítulo de la imagen
.rk-lightbox-info-descriptionInfoDescripción de la imagen
.rk-lightbox-slideSlideContenedor del slide
.rk-lightbox-imgSlideElemento de imagen
.rk-lightbox-video-containerVideoSlideContenedor del slide de vídeo (opcional)
.rk-lightbox-video-elementVideoSlideElemento de vídeo (opcional)
.rk-lightbox-video-posterVideoSlideImagen del póster del vídeo (opcional)

Temas

Sobrescribe cualquier propiedad personalizada de CSS --rk-lightbox-* en :root (o en cualquier ancestro de .rk-lightbox-overlay) para cambiar el tema. Las declaraciones directas en .rk-lightbox-overlay taparían los valores heredados, así que pon los cambios en un selector ancestro.

TokenPor defectoControla
--rk-lightbox-overlay-bg#000Color del fondo
--rk-lightbox-overlay-z9999z-index del overlay
--rk-lightbox-top-shade-height80pxAltura del degradado superior
--rk-lightbox-top-shade-bglinear-gradient(rgba(0,0,0,0.6), transparent)Degradado superior
--rk-lightbox-edge-padding16pxSeparación del borde de cerrar / navegación / controles
--rk-lightbox-btn-bgrgba(0, 0, 0, 0.5)Fondo por defecto de los botones de cerrar / navegación / pequeños
--rk-lightbox-btn-bg-hoverrgba(255, 255, 255, 0.2)Fondo al pasar el ratón de los botones de cerrar / navegación / pequeños
--rk-lightbox-btn-fg#fffColor del icono de los botones de cerrar / navegación / pequeños
--rk-lightbox-btn-size36pxTamaño de los botones pequeños (pantalla completa, etc.)
--rk-lightbox-close-size40pxTamaño del botón de cerrar
--rk-lightbox-nav-size48pxTamaño de las flechas de anterior / siguiente
--rk-lightbox-nav-opacity0.7Opacidad en reposo de las flechas de anterior / siguiente
--rk-lightbox-counter-bgrgba(0, 0, 0, 0.5)Fondo del chip del contador
--rk-lightbox-counter-fg#fffColor del texto del contador
--rk-lightbox-info-bglinear-gradient(transparent, rgba(0,0,0,0.8))Degradado del fondo del pie
--rk-lightbox-title-size18pxTamaño de letra del título
--rk-lightbox-description-size14pxTamaño de letra de la descripción
--rk-lightbox-video-bg#000Fondo de las bandas detrás del <video>
css
<style>
:root {
  --rk-lightbox-overlay-bg: #0f172a;
  --rk-lightbox-btn-bg: rgba(99, 102, 241, 0.65);
  --rk-lightbox-btn-bg-hover: rgba(168, 85, 247, 0.85);
  --rk-lightbox-info-bg: linear-gradient(
    transparent,
    rgba(99, 102, 241, 0.55) 60%,
    rgba(168, 85, 247, 0.85)
  );
}
</style>

Accesibilidad

La raíz del overlay es un diálogo modal (role="dialog", aria-modal="true"). Usa la prop aria-label para cambiar lo que anuncia el lector de pantalla; por defecto es "Image gallery". Cada slide lleva role="group", aria-roledescription="slide" y un aria-label derivado de la posición (por ejemplo, "Image 2 of 5").

El lightbox captura el foco al abrirse y lo devuelve al disparador al cerrarse. Tab y Mayús+Tab recorren los elementos enfocables de dentro; el foco que sale (un clic fuera, un foco por código) vuelve dentro. Está implementado con captureFocusForReturn y createFocusTrap de @reelkit/vue.

Atajos de teclado

TeclaAcción
ArrowLeftImagen anterior
ArrowRightImagen siguiente
EscapeCierra el lightbox (o sale de la pantalla completa si está activa)