Vue ガイド

@reelkit/vue でスライダーを作る方法を説明します。

タッチ優先
慣性とスナップのあるスワイプ
キーボード操作
矢印キーと Escape
ホイールスクロール
任意、デバウンス付き
仮想化
10,000 件以上、DOM には 3 つ
インジケーター
Instagram 風のドットのスクロール
プログラムからの操作
テンプレート参照で next()、prev()、goTo()
ループモード
終わりのない循環ナビゲーション
方向を選べる
縦でも横でも
Composition API
コンポーザブルと <script setup>

基本のスライダー

<Reel> コンポーネントはコアのスライダーコントローラーを包みます。#item スロットで各スライドを仮想化して描画し、マウントされるのは表示中のスライドだけです。

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

スライダー内の現在位置を示す、Instagram 風の進捗インジケーターを表示する任意のコンポーネントです。<Reel> の中に置くと、Vue の provide/inject を通じて親の count と active の値に自動で接続するので、手でつなぐ必要はありません。

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 — テンプレート参照

<Reel> コンポーネントは、テンプレート参照を通じて ReelExpose インターフェースを公開します。ref() で参照を保持し、next()、prev()、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>

横方向

左右にスワイプするスライダーにするには direction="horizontal" を指定します。インジケーターの向きも合わせてください。

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>

サイズの自動計測

size プロップは任意です。省略すると、<Reel> は ResizeObserver でコンテナーを自動計測し、CSS で決まるレイアウトに合わせます。コンテナーのサイズは親要素で決める必要があります(flex、grid、または 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>

トランジション

スライドのアニメーションをカスタマイズするには transition プロップを渡します。ReelKit にはツリーシェイキングできる 5 つのトランジションが用意されています。slideTransition(デフォルト)、fadeTransition、flipTransition、cubeTransition、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>

ループモード

loop プロップで、終わりのない循環ナビゲーションを有効にします。スライダーは最後のスライドから最初のスライドへ(逆も同様に)継ぎ目なく戻ります。

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

イベントコールバック

<Reel> コンポーネントは、スライダーの状態を追跡するためのイベントをいくつか発行します:

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>

組み込みの操作方法:

  • タッチ/スワイプ: ドラッグで移動し、慣性とスナップが効きます
  • キーボード: 矢印キーと Escape
  • マウスホイール: :enable-wheel="true" で有効にします
  • プログラムから: テンプレート参照で 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>

URL の状態

useOverlayUrlState はオーバーレイ用の URL 状態コントローラーを作り、そのまま返します。それを <LightboxUrlOverlay> の :controller プロップに渡します。開閉状態はアドレスバーが持つので、結び付けられたオーバーレイは自分で開き、開く操作は通常リンクになります。存在しないパラメーターへの最初の書き込みは履歴エントリーを 1 つ積み、以降の書き込みはそれを置き換えるので、ページを送っても戻るボタンが埋もれることはありません。コントローラーは手元に残しておき、value/position を読んだり、set(null) でプログラムから閉じたりするのに使います。これは、オーバーレイがスライドの切り替え時に内部で使っているのと同じ低レベルの書き込みです。

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>

オプションのオブジェクトは param、codec、locator(3 つとも必須)と、任意の adapter を受け取ります。codec と locator は同じ Id を共有する対なので、単純な ?photo=3 のギャラリーなら ...urlIndexKey(() => props.images.length) を展開すれば、両方が一度に返されます。urlIndexKey はパラメーターをスライドのインデックスに対応させ、getter が返す現在の件数で範囲を制限するので、古いまたは範囲外の ?photo=99 は拒否され、指されていないスライドを開く代わりに URL から自動で取り除かれます。数値ではなく getter を渡してください。Vue の setup は一度しか実行されないので、取り込んだ長さはページングされたフィードが伸びると古くなります。内部では createIndexLocator(locator の側)を包み、indexCodec と組み合わせています。ページングされたフィードや識別子で指すギャラリーは、代わりに独自の codec と locator の対を用意します。すべてのオプションの表は Vue API リファレンス にあります。

#item スロットのパターン

React のレンダープロップの代わりに、Vue ではスコープ付きの #item スロットを使います。これによって仮想化が可能になり、マウントされるのは表示中のスライドだけです。スロットのスコープは 3 つのプロパティを提供します:

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>

コンポーザブル

@reelkit/vue には、よくあるオーバーレイの場面のためのコンポーザブルがあります:

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>

ポイント

  • Composition API

    Reel、ReelIndicator、コンポーザブルを <script setup> に直接インポートします。プラグインの登録は必要ありません

  • スコープ付きの #item スロット

    React の itemBuilder プロップに相当する Vue の仕組みで、なじみのあるテンプレート構文のまま仮想化が使えます

  • テンプレート参照

    命令的なナビゲーションには ref<ReelExpose>() を使います。イベントコールバックは必要ありません

  • @after-change

    (index, rangeIndex) を発行します。UI を更新するために現在のインデックスを追跡します

  • provide/inject のコンテキスト

    ReelIndicator は Vue の provide/inject で親の Reel に自動で接続します。props を手でたどって渡す必要はありません

パフォーマンスのヒント

  • スライドのテンプレートを軽く保つ

    #item スロットは表示中のスライドごと(通常は同時に 3 枚)に実行されます。中で重い計算や深い入れ子の構造を使わないでください。

  • 端に近づいたらデータを読み込む

    @after-change でユーザーが終わりに近づいたことを検知し、スライドが尽きる前に次のバッチを取得します。これで無限スクロールのフィードが作れます。

  • 命令的な状態には ref を使う

    ReelExpose の参照と現在のインデックスを Vue の ref() に保持すると、不要な再描画なしに細かなリアクティビティが得られます。

  • スクロールするページではホイールを無効にする

    スクロールするレイアウトの中にスライダーを埋め込むときは、ページのスクロールを奪わないよう :enable-wheel="false" を指定してください。

次のステップ