バーチャルスクロール
※この記事は2025/09/15時点の情報です。
前回に引き続き「vue-virtual-scroller」ライブラリを使用してバーチャルスクロールを実装してみました。
今回はTABLEタグの代わりにDIVタグを使用し、CSSでTABLEと同じ見た目にしています。
また、ヘッダーは固定とし、行を選択するとその行のデータをポップアップで表示させています。
「vue-virtual-scroller」ライブラリは下記コマンドでインストールします。
npm install --save vue-virtual-scroller@next
インストールはこれで完了ですが、実装して「モジュール 'vue-virtual-scroller' の宣言ファイルが見つかりませんでした。」 というエラーになる場合はTypeScriptがvue-virtual-scrollerの型定義ファイルを見つけられないことが原因です。
この場合はtypesフォルダ内に新しいファイルを作成します。
例: types/vue-virtual-scroller.d.ts
declare module 'vue-virtual-scroller';
pages/virtualScroll.vue
<template>
<ClientOnly>
<div class="scroll-x-wrapper">
<!-- 🔍 検索入力フォーム -->
<div class="filter-bar">
<input
v-model="searchQuery"
type="text"
placeholder="キーワードで検索"
class="filter-input"
/>
</div>
<!-- 固定ヘッダー -->
<div class="header-row">
<div
v-for="(col, idx) in columns"
:key="col.key"
class="header-cell"
draggable="true"
@dragstart="onDragStart(idx)"
@dragover.prevent
@drop="onDrop(idx)"
@click="sortBy(col.key)"
>
{{ col.label }}
<span v-if="sortKey === col.key">
{{ sortAsc ? '▲' : '▼' }}
</span>
</div>
</div>
<!-- 仮想スクロール本体 -->
<RecycleScroller
class="body-wrapper"
:items="sortedItems"
:item-size="rowHeight"
key-field="id"
:buffer="400"
>
<template #default="{ item }">
<div
class="grid-row"
:class="{ selected: selectedRowId === item.id }"
@click="selectRow(item)"
>
<div
v-for="col in columns"
:key="col.key"
class="grid-cell"
>
{{ item[col.key] }}
</div>
</div>
</template>
</RecycleScroller>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
type Item = {
id: number
name: string
age: number
city: string
col5: string
col6: string
col7: string
col8: string
col9: string
}
type Column = { key: keyof Item; label: string }
const columns = ref<Column[]>([
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'age', label: 'Age' },
{ key: 'city', label: 'City' },
{ key: 'col5', label: 'Col5' },
{ key: 'col6', label: 'Col6' },
{ key: 'col7', label: 'Col7' },
{ key: 'col8', label: 'Col8' },
{ key: 'col9', label: 'Col9' },
])
const rows: Item[] = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
name: `Name-${i + 1}`,
age: 20 + (i % 60),
city: `City-${i % 100}`,
col5: `V5-${i}`,
col6: `V6-${i}`,
col7: `V7-${i}`,
col8: `V8-${i}`,
col9: `V9-${i}`,
}))
// 🔍 フィルター検索
const searchQuery = ref('')
const filteredItems = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
if (!q) return rows
return rows.filter(item =>
Object.values(item).some(v =>
String(v).toLowerCase().includes(q)
)
)
})
// ソート
const sortKey = ref<keyof Item>('id')
const sortAsc = ref(true)
function sortBy(key: keyof Item) {
if (sortKey.value === key) sortAsc.value = !sortAsc.value
else {
sortKey.value = key
sortAsc.value = true
}
}
const sortedItems = computed(() => {
const k = sortKey.value
return [...filteredItems.value].sort((a, b) => {
const A = a[k]
const B = b[k]
return (A > B ? 1 : A < B ? -1 : 0) * (sortAsc.value ? 1 : -1)
})
})
// 列ドラッグ
let dragIndex = -1
function onDragStart(idx: number) { dragIndex = idx }
function onDrop(idx: number) {
if (dragIndex < 0 || dragIndex === idx) return
const moved = columns.value.splice(dragIndex, 1)[0]
columns.value.splice(idx, 0, moved)
dragIndex = -1
}
// 行選択
const selectedRowId = ref<number | null>(null)
function selectRow(item: Item) {
selectedRowId.value = item.id
alert(JSON.stringify(item, null, 2))
}
const rowHeight = 30
</script>
<style scoped>
.scroll-x-wrapper {
overflow-x: auto;
border: 1px solid #ccc;
max-height: 600px;
position: relative;
min-width: max-content;
}
/* 🔍 検索バー */
.filter-bar {
padding: 4px;
background: #fafafa;
border-bottom: 1px solid #ccc;
}
.filter-input {
width: 200px;
padding: 4px 8px;
font-size: 14px;
}
/* ヘッダー固定 */
.header-row {
display: flex;
position: sticky;
top: 0;
z-index: 10;
background: #f0f0f0;
}
.header-cell {
flex: 0 0 120px;
padding: 0 8px;
height: 30px;
line-height: 30px;
border: 1px solid #ccc;
font-weight: bold;
user-select: none;
white-space: nowrap;
cursor: pointer;
}
/* 仮想スクロール本体 */
.body-wrapper {
height: 560px;
overflow-y: auto;
}
/* 行とセル */
.grid-row {
display: flex;
cursor: pointer;
}
.grid-cell {
flex: 0 0 120px;
padding: 0 8px;
border: 1px solid #ccc;
height: 30px;
line-height: 30px;
white-space: nowrap;
}
/* 選択行 */
.selected {
background-color: #cce5ff;
}
</style>
仮想スクロールで大量データでも高速に表示することができ、
ドラッグ&ドロップで列の入れ替えもできるようになっていますので実用性が高いのではないかと思います。
今回はここで終了ですが、今回のサンプルプログラムは理解できましたか?
他にも様々な設定ができるので興味ある方はいろいろ試してみてくださいね!