Building a Dynamic Stack Grid System in Vue 3
Creating responsive grid layouts has always been a challenge in web development. While CSS Grid and Flexbox have made tremendous strides in this area, they often fall short when dealing with dynamic content and complex responsive behaviors. This gap led me to develop vue-stack-grid, a lightweight solution that would later inspire its React counterpart.
The Challenge of Dynamic Layouts
Modern web applications demand layouts that can handle dynamic content gracefully. Whether it's a photo gallery, a portfolio, or a dashboard, we need grid systems that can:
- Automatically adjust to various screen sizes
- Handle items of different heights
- Maintain performance with frequent updates
- Provide a smooth user experience
Traditional solutions often involve complex calculations or heavy dependencies. I wanted something different: a pure Vue solution that would be both lightweight and powerful.
Leveraging Vue 3's Composition API
The Composition API in Vue 3 provided the perfect foundation for building such a system. Here's how I structured the core functionality:
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({
items: Array,
columnMinWidth: Number,
gutterWidth: {
type: Number,
default: 0
},
gutterHeight: {
type: Number,
default: 0
},
})
This setup allows for a clean, functional approach to managing the grid's state and behavior. The Composition API's reactivity system makes it easy to handle dynamic updates efficiently.
The Mathematics of Responsive Grids
The heart of the system lies in its mathematical approach to layout calculation. Let's break down the key algorithms:
function calculateColumnCount(containerWidth, columnMinWidth, gutterWidth) {
return Math.max(
Math.floor((containerWidth + gutterWidth) / (columnMinWidth + gutterWidth)),
1
)
}
function calculateColumnWidth(containerWidth, columnCount, gutterWidth) {
return (containerWidth - gutterWidth * (columnCount - 1)) / columnCount
}
These functions work together to:
- Determine the optimal number of columns based on container width
- Calculate the exact width of each column while maintaining consistent gutters
- Ensure the layout remains responsive at all breakpoints
Smart Position Management
One of the most challenging aspects was managing the position of each item in the grid. The solution came in the form of a column-tracking system:
function generateBaseColumns(columnCount, columnWidth, gutterWidth) {
return Array.from({ length: columnCount }, (_, i) => ({
x: i * (columnWidth + gutterWidth),
h: 0
}))
}
This approach tracks the height of each column and places new items in the shortest column, creating a natural, balanced layout.
Template Structure and Style Integration
The template structure is intentionally minimal, leveraging Vue's slot system for maximum flexibility:
<template>
<div ref="container" class="stack-grid-container">
<div v-for="(item, key) in items" :key="key" class="stack-item">
<slot name="item" v-bind="{item, key}"/>
</div>
</div>
</template>
Coupled with strategic CSS:
.stack-grid-container {
display: block;
position: relative;
width: 100%;
}
.stack-item {
position: absolute;
top: 0;
left: 0;
}
This combination provides the foundation for the dynamic positioning system while maintaining clean, maintainable code.
Performance Optimizations
Performance was a key consideration throughout development. Several strategies were employed:
- Efficient Reflow Management
function update() {
nextTick(reflow)
}
- Smart Event Handling
onMounted(() => {
window.addEventListener('resize', reflow)
update()
})
onUnmounted(() => {
window.removeEventListener('resize', reflow)
})
- Optimized Column Updates
function arrangeItems(children, cols, columnWidth) {
if (!children || !cols || !cols.length) return
Array.from(children).forEach((child) => {
const { index } = cols.reduce((acc, col, idx) => {
if (acc.minHeight === null || col.h < acc.minHeight) {
return { index: idx, minHeight: col.h }
}
return acc
}, { index: 0, minHeight: null })
// Position updates...
})
}
Real-World Application
The component has proven its worth in various scenarios:
- Image galleries with varying aspect ratios
- Dynamic content feeds
- Portfolio layouts
- Dashboard widgets
Its flexibility comes from the simple but powerful API:
<template>
<StackGrid
:items="items"
:column-min-width="200"
:gutter-width="10"
:gutter-height="10"
>
<template #item="{ item }">
<div>{{ item.content }}</div>
</template>
</StackGrid>
</template>
Looking Forward: The React Port
The vue-stack-grid led to an interesting development: the creation of a React version. The mathematical principles and core functionality remained the same, but the implementation needed to be adapted for React's component model and lifecycle methods. That journey, however, deserves its own article.
Conclusion
Building vue-stack-grid was an exercise in finding the right balance between flexibility and simplicity. By leveraging Vue 3's Composition API and solid mathematical principles, we created a solution that's both powerful and easy to use.
The component is available on npm (@crob/vue-stack-grid) and the source code is open for contributions on GitHub. Whether you're building a simple portfolio or a complex dashboard, I hope this tool makes your development process a little bit easier.
In the next article, I'll discuss how this Vue component was adapted for React, highlighting the challenges and insights gained from porting a component between two major frontend frameworks.