Vue Lightbox
Lightbox de galería de imágenes y vídeos a pantalla completa para Vue 3, con @reelkit/vue-lightbox.
Características
Instalación
npm install @reelkit/vue-lightbox @reelkit/vue lucide-vue-nextNo olvides importar los estilos:
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.
<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 | Ámbito | Descripción |
|---|---|---|
| #slide | SlideSlotScope | Sustituye el contenido de cada slide (necesario para los slides de vídeo) |
| #controls | ControlsSlotScope | Sustituye la barra de controles superior (cerrar, contador, pantalla completa) |
| #navigation | NavigationSlotScope | Sustituye las flechas de anterior y siguiente |
| #info | InfoSlotScope | Sustituye el overlay inferior con degradado del título y la descripción |
| #loading | LoadingSlotScope | Indicador de carga propio |
| #error | ErrorSlotScope | Indicador de error propio |
<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.
<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.
<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.
<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=3abre 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.
<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.
<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.
<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.
<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
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
isOpen | boolean | obligatorio | Controla la visibilidad; con false el overlay se quita del DOM. Se puede enlazar con v-model:is-open. |
items | LightboxItem[] | obligatorio | Array de elementos (imágenes o vídeos) |
initialIndex | number | 0 | Índice, empezando en cero, del elemento visible al principio |
transitionFn | TransitionTransformFn | slideTransition | Función de transición entre slides. Importa una incluida (slideTransition, flipTransition, lightboxFadeTransition, lightboxZoomTransition) o pasa una propia. Si se omite, se usa slideTransition. |
showInfo | boolean | true | Indica si se renderiza el overlay de información con el título y la descripción |
showControls | boolean | true | Indica si se renderiza la barra de controles superior (cerrar, contador, pantalla completa) |
showNavigation | boolean | true | Indica si se renderizan las flechas de anterior y siguiente (solo en escritorio) |
transitionDuration | number | 300 | Duración de la animación de los slides en ms |
swipeDistanceFactor | number | 0.12 | Fracció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 |
loop | boolean | false | Indica si el slider pasa del último slide al primero |
enableNavKeys | boolean | true | Activa la navegación con las teclas de flecha |
enableWheel | boolean | true | Activa la navegación con la rueda del ratón |
wheelDebounceMs | number | 200 | Duración del debounce de los eventos de la rueda en ms |
ariaLabel | string | '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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
controller | UrlStateController | obligatorio | Controlador 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
| Evento | Datos | Descripción |
|---|---|---|
close | void | Se emite cuando el usuario cierra el lightbox |
slide-change | number | Se emite con el índice del nuevo slide activo después de un cambio |
api-ready | LightboxApi | Se emite cuando el slider está listo y expone la API imperativa |
update:is-open | boolean | Se emite al cerrar; permite usar v-model:is-open |
Interfaz LightboxItem
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
src | string | sí | URL de la imagen o el vídeo |
type | 'image' | 'video' | no | Tipo de elemento. Por defecto es 'image' |
poster | string | no | Imagen en miniatura de los vídeos |
title | string | no | Título que se muestra en el overlay de información |
description | string | no | Descripción que se muestra debajo del título |
width | number | no | Ancho intrínseco de la imagen en píxeles |
height | number | no | Alto intrínseco de la imagen en píxeles |
Tipos del ámbito de slot
| Tipo | Campos |
|---|---|
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ón | Descripción |
|---|---|
slideTransition | Por defecto. Desplazamiento horizontal entre slides; se reexporta desde @reelkit/vue. |
lightboxFadeTransition | Fundido cruzado con un ligero desplazamiento horizontal. Propia de @reelkit/vue-lightbox. |
flipTransition | Volteo 3D sobre el eje Y; se reexporta desde @reelkit/vue. |
lightboxZoomTransition | El slide entrante escala del 70 % al 100 % con fundido. Propia de @reelkit/vue-lightbox. |
<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
| Callback | Tipo | Descripción |
|---|---|---|
onReady | () => void | Indica que el contenido del slide se ha cargado bien (por ejemplo, la imagen se ha decodificado) |
onWaiting | () => void | Indica que el contenido del slide se está cargando (muestra el spinner) |
onError | () => void | Indica que el contenido del slide no se ha podido cargar (muestra el icono de error) |
Conectar los callbacks en #slide
<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.
<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.
<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.
| Clase | Componente | Descripción |
|---|---|---|
.rk-lightbox-overlay | Overlay | Contenedor raíz (fondo a pantalla completa) |
.rk-lightbox-top-shade | Overlay | Degradado superior detrás de los controles |
.rk-lightbox-spinner | Overlay | Spinner de carga por defecto |
.rk-lightbox-error | Overlay | Contenedor del estado de error (imagen rota) |
.rk-lightbox-error-text | Overlay | Texto del estado de error |
.rk-lightbox-controls-left | Controls | Contenedor de los controles de arriba a la izquierda |
.rk-lightbox-btn | Controls | Botón de control (pantalla completa, sonido, etc.) |
.rk-lightbox-close | Controls | Botón de cerrar |
.rk-lightbox-counter | Controls | Chip del contador de imágenes |
.rk-lightbox-nav | Navigation | Flecha de navegación (anterior y siguiente) |
.rk-lightbox-nav-prev | Navigation | Flecha de anterior |
.rk-lightbox-nav-next | Navigation | Flecha de siguiente |
.rk-lightbox-info | Info | Contenedor del título y la descripción |
.rk-lightbox-info-title | Info | Título de la imagen |
.rk-lightbox-info-description | Info | Descripción de la imagen |
.rk-lightbox-slide | Slide | Contenedor del slide |
.rk-lightbox-img | Slide | Elemento de imagen |
.rk-lightbox-video-container | VideoSlide | Contenedor del slide de vídeo (opcional) |
.rk-lightbox-video-element | VideoSlide | Elemento de vídeo (opcional) |
.rk-lightbox-video-poster | VideoSlide | Imagen 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.
| Token | Por defecto | Controla |
|---|---|---|
--rk-lightbox-overlay-bg | #000 | Color del fondo |
--rk-lightbox-overlay-z | 9999 | z-index del overlay |
--rk-lightbox-top-shade-height | 80px | Altura del degradado superior |
--rk-lightbox-top-shade-bg | linear-gradient(rgba(0,0,0,0.6), transparent) | Degradado superior |
--rk-lightbox-edge-padding | 16px | Separación del borde de cerrar / navegación / controles |
--rk-lightbox-btn-bg | rgba(0, 0, 0, 0.5) | Fondo por defecto de los botones de cerrar / navegación / pequeños |
--rk-lightbox-btn-bg-hover | rgba(255, 255, 255, 0.2) | Fondo al pasar el ratón de los botones de cerrar / navegación / pequeños |
--rk-lightbox-btn-fg | #fff | Color del icono de los botones de cerrar / navegación / pequeños |
--rk-lightbox-btn-size | 36px | Tamaño de los botones pequeños (pantalla completa, etc.) |
--rk-lightbox-close-size | 40px | Tamaño del botón de cerrar |
--rk-lightbox-nav-size | 48px | Tamaño de las flechas de anterior / siguiente |
--rk-lightbox-nav-opacity | 0.7 | Opacidad en reposo de las flechas de anterior / siguiente |
--rk-lightbox-counter-bg | rgba(0, 0, 0, 0.5) | Fondo del chip del contador |
--rk-lightbox-counter-fg | #fff | Color del texto del contador |
--rk-lightbox-info-bg | linear-gradient(transparent, rgba(0,0,0,0.8)) | Degradado del fondo del pie |
--rk-lightbox-title-size | 18px | Tamaño de letra del título |
--rk-lightbox-description-size | 14px | Tamaño de letra de la descripción |
--rk-lightbox-video-bg | #000 | Fondo de las bandas detrás del <video> |
<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
| Tecla | Acción |
|---|---|
ArrowLeft | Imagen anterior |
ArrowRight | Imagen siguiente |
Escape | Cierra el lightbox (o sale de la pantalla completa si está activa) |