A waterfall layout is ideal for lists where items have inconsistent heights (e.g., images of different sizes or varying text lengths). This implementation uses a pure JavaScript approach with absolute positioning to place items in the shortest available column.
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, withDefaults } from 'vue';
const props = withDefaults(
defineProps<{
list: any[];
columnWidth?: number; // Column width
columnGap?: number; // Gap between columns
}>(),
{
columnWidth: 240,
columnGap: 24,
}
);
const wrapper = ref<HTMLElement | null>(null);
const content = ref<HTMLElement | null>(null);
const flowHeight: number[] = [];
const flowDraw = () => {
if (!content.value) return;
const columnCount = getColumnCount();
flowHeight.length = columnCount;
for (let i = 0; i < columnCount; i++) {
flowHeight[i] = 0;
}
const itemW = props.columnWidth + props.columnGap;
content.value.style.width = itemW * columnCount - props.columnGap + 'px';
const doms = content.value.querySelectorAll('.WaterfallItem');
doms.forEach((dom: any) => {
const minIdx = getMinIndex(flowHeight);
dom.style.left = `${minIdx * itemW}px`;
dom.style.top = `${flowHeight[minIdx]}px`;
flowHeight[minIdx] += dom.offsetHeight;
});
content.value.style.height = Math.max(...flowHeight) + 'px';
};
const getColumnCount = (): number => {
if (!wrapper.value) return 0;
const itemW = props.columnWidth + props.columnGap;
const num = (wrapper.value.offsetWidth + props.columnGap) / itemW;
return Math.min(Math.floor(num), props.list.length);
};
const getMinIndex = (list: number[]) => {
const min = Math.min(...list);
return list.indexOf(min);
};
let timer: number | null = null;
const onResize = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
flowDraw();
}, 300);
};
onMounted(() => {
flowDraw();
window.addEventListener('resize', onResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize);
});
</script>
<template>
<div class="WaterfallList" ref="wrapper">
<div class="WaterfallContent" ref="content">
<div class="WaterfallItem" v-for="(item, index) in list" :key="index">
<slot name="item" :index="index" :item="item"></slot>
</div>
</div>
</div>
</template>
<style lang="sass" scoped>
.WaterfallContent
margin-left: auto
margin-right: auto
position: relative
.WaterfallItem
padding-bottom: 24px
width: v-bind("columnWidth + 'px'")
position: absolute
top: 0
left: 0
</style>