I have been trying to create a dynamic grid using tanstack virtualized rows (the number of columns being tied to window width)
The issue is that my rows expand in height due to window resize, but my visualizer uses a fixed estimate height, making the scroll 'jump' and not smooth.
I have a minimal working example setup here if anyone could help me figure out how to measure the elements. https://codesandbox.io/p/devbox/txkp7k
I think I have tried a few dozen versions and ways to measure the height of the elements but nothing has worked. The fix would be to measure the height of the elements on resize or update, but I couldn't get it working with tanstack at all.
This is my virtual grid:
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useBreakpoints } from '@vueuse/core';
import { useVirtualizer } from '@tanstack/vue-virtual';
import { totalCardCount, cardIDs, cardMap } from '../data/generate';
import CardComponent from './CardComponent.vue';
defineProps<{
isVaultLoaded: boolean;
}>();
const containerRef = ref<HTMLElement | null>(null);
const breakpoints = useBreakpoints({ sm: 740, md: 1020, lg: 1340, xl: 1640 });
const columnCount = computed(() => {
if (breakpoints.smaller('sm').value) return 1;
if (breakpoints.between('sm', 'md').value) return 2;
if (breakpoints.between('md', 'lg').value) return 3;
if (breakpoints.between('lg', 'xl').value) return 4;
return 5;
});
const rowCount = computed(() => Math.ceil(totalCardCount / columnCount.value));
const virtualizerOptions = computed(() => ({
count: rowCount.value,
getScrollElement: () => containerRef.value,
estimateSize: () => 480, // An estimate of a single row's height
overscan: 5,
}));
const rowVirtualizer = useVirtualizer(virtualizerOptions);
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems());
</script>
<template>
<div v-if="isVaultLoaded" ref="containerRef" class="grid-container hide-scrollbar">
<div :style="{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }">
<div
class="grid-window @container"
:style="{
'--column-count': columnCount,
transform: `translateY(${virtualRows[0]?.start ?? 0}px)`,
}"
>
<template v-for="row in virtualRows" :key="row.key">
<template v-for="colIndex in columnCount" :key="colIndex">
<template v-if="(row.index * columnCount + colIndex - 1) < totalCardCount">
<CardComponent
:key="cardIDs[row.index * columnCount + colIndex - 1]"
v-if="cardMap.has(cardIDs[row.index * columnCount + colIndex - 1])"
:item="cardMap.get(cardIDs[row.index * columnCount + colIndex - 1])!"
/>
</template>
</template>
</template>
</div>
</div>
</div>
<div v-else class="placeholder-text">
<p>Please select and load a vault to view cards.</p>
</div>
</template>
<style scoped>
.grid-container {
width: 100%;
height: 100%;
overflow-y: auto;
contain: strict;
}
.grid-window {
position: absolute;
top: 0;
left: 0;
width: 100%;
display: grid;
grid-template-columns: repeat(var(--column-count), 1fr);
gap: 1rem;
padding: 1rem;
grid-template-rows: auto auto auto auto;
}
.placeholder-text {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #888;
font-style: italic;
}
.hide-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
</style>