Guía de Vue

Aprende a crear sliders con @reelkit/vue.

Táctil ante todo
Deslizamiento con inercia y ajuste
Teclado
Flechas + Escape
Rueda del ratón
Opcional, con debounce
Virtualizado
Más de 10.000 elementos, 3 en el DOM
Indicadores
Puntos que se desplazan al estilo de Instagram
API por código
next(), prev(), goTo() mediante template ref
Modo bucle
Navegación circular infinita
Dirección
Vertical u horizontal
Composition API
<script setup> con composables

Slider básico

El componente <Reel> envuelve el controlador del slider del core. Usa el slot #item para renderizar cada slide con virtualización: solo se montan los slides visibles.

App.vue
<script setup lang="ts">
import { Reel, ReelIndicator } from '@reelkit/vue';

const items = [
  { title: 'Virtualized', subtitle: 'Only 3 slides in DOM', color: '#6366f1' },
  { title: 'Touch First', subtitle: 'Native swipe gestures', color: '#8b5cf6' },
  { title: 'Zero Deps', subtitle: 'Tiny bundle size', color: '#7c3aed' },
  { title: 'Keyboard Nav', subtitle: 'Full a11y support', color: '#ec4899' },
  { title: 'SSR Ready', subtitle: 'Works everywhere', color: '#14b8a6' },
  { title: '60fps', subtitle: 'Smooth animations', color: '#f59e0b' },
];

const onAfterChange = (index: number) => {
  console.log('Current index:', index);
};
</script>

<template>
  <Reel
    :count="items.length"
    style="width: 100%; height: 100dvh"
    direction="vertical"
    :enable-wheel="true"
    @after-change="onAfterChange"
  >
    <template #item="{ index, size }">
      <div
        :style="{
          width: size[0] + 'px',
          height: size[1] + 'px',
          background: items[index].color,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          color: '#fff',
        }"
      >
        <div style="font-size: 1.5rem; font-weight: bold">
          {{ items[index].title }}
        </div>
        <div style="font-size: 0.875rem; opacity: 0.8">
          {{ items[index].subtitle }}
        </div>
      </div>
    </template>

    <div style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); z-index: 10">
      <ReelIndicator direction="vertical" />
    </div>
  </Reel>
</template>

ReelIndicator

Componente opcional que muestra indicadores de progreso al estilo de Instagram con la posición actual en el slider. Colocado dentro de un <Reel>, se conecta solo a los valores count y active del padre a través del provide/inject de Vue, sin conectar nada a mano.

vue-html
<!-- Auto-connect: count and active are inherited from parent Reel -->
<Reel :count="10" :size="[400, 600]">
  <template #item="{ index, size }"> ... </template>
  <ReelIndicator direction="vertical" />
</Reel>

<!-- Manual usage: pass count and active explicitly (e.g. outside a Reel) -->
<ReelIndicator :count="10" :active="currentIndex" />

API imperativa — template ref

El componente <Reel> expone una interfaz ReelExpose a través de un template ref. Usa ref() para guardar la referencia y llamar a métodos imperativos como next(), prev() y goTo().

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Reel, type ReelExpose } from '@reelkit/vue';

const items = [
  { title: 'Slide 1', color: '#6366f1' },
  { title: 'Slide 2', color: '#8b5cf6' },
  { title: 'Slide 3', color: '#ec4899' },
];

const sliderRef = ref<ReelExpose | null>(null);
const currentIndex = ref(0);

const onAfterChange = (index: number) => {
  currentIndex.value = index;
};
</script>

<template>
  <Reel
    ref="sliderRef"
    :count="items.length"
    style="width: 100%; height: 100dvh"
    direction="vertical"
    :enable-wheel="true"
    @after-change="onAfterChange"
  >
    <template #item="{ index, size }">
      <div
        :style="{
          width: size[0] + 'px',
          height: size[1] + 'px',
        }"
      >
        {{ items[index].title }}
      </div>
    </template>
  </Reel>

  <div style="position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%)">
    <button
      :disabled="currentIndex === 0"
      @click="sliderRef?.prev()"
    >
      Prev
    </button>
    <button
      :disabled="currentIndex === items.length - 1"
      @click="sliderRef?.next()"
    >
      Next
    </button>
    <button @click="sliderRef?.goTo(2)">Go to 3</button>
  </div>
</template>

Dirección horizontal

Pon direction="horizontal" para un slider que se desliza a izquierda y derecha. La dirección del indicador debería coincidir.

vue-html
<Reel
  :count="items.length"
  :size="[400, 300]"
  direction="horizontal"
>
  <template #item="{ index, size }">
    <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
      {{ items[index].title }}
    </div>
  </template>

  <ReelIndicator direction="horizontal" />
</Reel>

Tamaño automático

La prop size es opcional. Sin ella, <Reel> mide su contenedor con ResizeObserver y se adapta al diseño que marque el CSS. El tamaño del contenedor lo tiene que dar su padre (por ejemplo, flex, grid o dimensiones explícitas en CSS).

vue-html
<!-- Explicit size (fixed) -->
<Reel :count="items.length" :size="[400, 600]">
  <template #item="{ index, size }"> ... </template>
</Reel>

<!-- Auto-size (responsive — sized by CSS) -->
<Reel :count="items.length" style="width: 100%; height: 100dvh">
  <template #item="{ index, size }"> ... </template>
</Reel>

Transiciones

Pasa una prop transition para personalizar la animación de los slides. ReelKit incluye cinco transiciones que admiten tree-shaking: slideTransition (por defecto), fadeTransition, flipTransition, cubeTransition y zoomTransition.

vue
<script setup lang="ts">
import { Reel, cubeTransition } from '@reelkit/vue';
</script>

<template>
  <Reel
    :count="items.length"
    :size="[400, 600]"
    :transition="cubeTransition"
  >
    <template #item="{ index, size }"> ... </template>
  </Reel>
</template>

Modo bucle

Activa la navegación circular infinita con la prop loop. El slider pasa sin cortes del último slide al primero (y al revés).

vue-html
<Reel
  :count="items.length"
  :size="[400, 600]"
  :loop="true"
>
  <template #item="{ index, size }"> ... </template>
</Reel>

Callbacks de eventos

El componente <Reel> emite varios eventos para seguir el estado del slider:

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

const onBeforeChange = (index: number, nextIndex: number, rangeIndex: number) => {
  console.log('Transitioning from', index, 'to', nextIndex);
};

const onAfterChange = (index: number, rangeIndex: number) => {
  console.log('Arrived at slide', index);
};

const onSlideDragStart = (index: number) => {
  console.log('Started dragging slide', index);
};

const onSlideDragEnd = (index: number) => {
  console.log('Stopped dragging slide', index);
};
</script>

<template>
  <Reel
    :count="20"
    style="width: 100%; height: 100dvh"
    @before-change="onBeforeChange"
    @after-change="onAfterChange"
    @slide-drag-start="onSlideDragStart"
    @slide-drag-end="onSlideDragEnd"
  >
    <template #item="{ index, size }"> ... </template>
  </Reel>
</template>

Métodos de navegación incluidos:

  • Táctil / deslizar: arrastra para navegar con inercia y ajuste al slide
  • Teclado: teclas de flecha y Escape
  • Rueda del ratón: se activa con :enable-wheel="true"
  • Por código: usa un template ref para acceder a next(), prev(), goTo()
vue
<script setup lang="ts">
import { ref } from 'vue';
import { Reel, type ReelExpose } from '@reelkit/vue';

const sliderRef = ref<ReelExpose | null>(null);
</script>

<template>
  <Reel
    ref="sliderRef"
    :count="10"
    :size="[400, 600]"
  >
    <template #item="{ index, size }">
      <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
        Slide {{ index + 1 }}
      </div>
    </template>
  </Reel>

  <button @click="sliderRef?.prev()">Prev</button>
  <button @click="sliderRef?.next()">Next</button>
  <button @click="sliderRef?.goTo(5)">Go to 6</button>
</template>

Estado en la URL

useOverlayUrlState crea un controlador de estado en la URL para un overlay y lo devuelve entero; después se lo pasas a un <LightboxUrlOverlay> en su prop :controller. La barra de direcciones es dueña del estado abierto, así que un overlay vinculado se abre solo y lo habitual es abrirlo con un enlace. La primera escritura de un parámetro ausente añade una entrada al historial y las siguientes la sustituyen, así que pasar slides nunca entierra el botón de volver. Guarda el controlador para leer value/position y para cerrar por código con set(null), la misma escritura de bajo nivel que usa el overlay internamente al cambiar de slide.

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

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

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

<template>
  <!-- Opening is a link — 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 objeto de opciones recibe param, codec y locator (los tres obligatorios), más un adapter opcional. El codec y el locator son un par a juego que comparte el mismo Id, así que para una galería simple con ?photo=3 usa ...urlIndexKey(() => props.images.length), que devuelve las dos mitades a la vez. urlIndexKey convierte el parámetro en el índice de un slide y lo limita con el recuento actual que devuelve el getter, así que un ?photo=99 desfasado o fuera de rango se rechaza y se limpia solo de la URL en lugar de abrir un slide que nunca se nombró. Pasa un getter y no un número: el setup de Vue se ejecuta una sola vez, así que una longitud capturada quedaría desfasada mientras crece un feed paginado. Envuelve createIndexLocator (la mitad del locator) y lo combina con indexCodec. Un feed paginado o una galería por identidad aporta su propio par de codec + locator. La tabla completa de opciones está en la referencia de la API de Vue.

Patrón del slot #item

En lugar de la render prop de React, Vue usa el scoped slot #item. Esto hace posible la virtualización: solo se montan los slides visibles. El ámbito del slot ofrece tres propiedades:

vue-html
<template #item="{ index, indexInRange, size }">
  <!--
    index        : number          — absolute slide index (0 to count-1)
    indexInRange  : number          — position in visible window (0, 1, or 2)
    size          : [number, number] — [width, height] of the container
  -->
  <MySlide
    :data="items[index]"
    :style="{ width: size[0] + 'px', height: size[1] + 'px' }"
  />
</template>

Composables

@reelkit/vue incluye composables para los casos de overlay más comunes:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useBodyLock, useFullscreen, useReelContext } from '@reelkit/vue';

// Lock body scroll when an overlay is open
const isOpen = ref(true);
useBodyLock(isOpen);

// Fullscreen API with cross-browser support
const containerRef = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen({ elementRef: containerRef });

// Access parent Reel context (when inside a Reel)
const reelContext = useReelContext();
// reelContext?.index  — active slide index signal
// reelContext?.count  — total slide count signal
// reelContext?.goTo() — navigate programmatically

// Bridge a core Subscribable into a reactive Vue ref
import { toVueRef } from '@reelkit/vue';
const index = toVueRef(reelContext!.index); // Ref<number> — re-renders on change
</script>

Puntos clave

  • Composition API

    Importa Reel, ReelIndicator y los composables directamente en tu <script setup>, sin registrar ningún plugin

  • Scoped slot #item

    El equivalente en Vue de la prop itemBuilder de React: hace posible la virtualización con la sintaxis de plantillas de siempre

  • Template ref

    Usa ref<ReelExpose>() para navegar de forma imperativa, sin callbacks de eventos

  • @after-change

    Emite (index, rangeIndex): sigue el índice actual para actualizar la interfaz

  • Contexto con provide/inject

    ReelIndicator se conecta solo al Reel padre con el provide/inject de Vue, sin pasar props a mano por varios niveles

Consejos de rendimiento

  • Mantén ligeras las plantillas de los slides

    El slot #item se ejecuta para cada slide visible (normalmente 3 a la vez). Evita dentro cálculos pesados o estructuras muy anidadas.

  • Carga datos cerca del final

    Usa @after-change para detectar cuándo el usuario se acerca al final y pide el siguiente lote antes de que se acaben los slides; así puedes tener feeds con scroll infinito.

  • Usa refs para el estado imperativo

    Guarda la referencia a ReelExpose y el índice actual en ref() de Vue para tener una reactividad precisa sin re-renderizados innecesarios.

  • Desactiva la rueda en páginas con scroll

    Pon :enable-wheel="false" cuando el slider esté dentro de un diseño con scroll para no capturar el desplazamiento de la página.

Siguientes pasos