Skip to main content

Grocery Cart

Vue 3 shopping cart with smart filtering, responsive grid, and real-time cart calculations

Role: Personal ProjectPeriod: 20235 min read
Vue 3TypeScriptTailwind CSSVite
Vue 3
Composition API
Smart
multi-criteria filters
Realtime
cart calculations

Pet project — built to explore Vue 3 patterns


A feature-rich grocery shopping application built with Vue 3, TypeScript, and Tailwind CSS. Browse products, filter by multiple criteria, and manage a cart with real-time price calculations — all without pulling in a dedicated state management library.

Grocery Cart app mockup

Motivation

Most shopping cart demos tend to fall into one of two extremes: they are either overly simplistic single-file components that don't reflect real-world complexity, or they are framework-specific boilerplate projects that assume a particular state management library. This project was an exercise in finding the middle ground — exploring how far you can push Vue 3's Composition API before you genuinely need Pinia or Vuex.

The core question was: can you build a production-quality shopping cart with real-time filtering, reactive totals, and multi-criteria sorting using only Vue 3's built-in reactivity primitives? The answer turned out to be yes — and the composables pattern made the code more testable, not less.

Vue 3
Composition API
TypeScript
Type safety
Tailwind
Utility-first CSS
Vite
Build tool

How It Works

The application separates concerns into three independent composables, each responsible for a single domain. The product listing layer fetches and sorts items from a static product catalog. The filter layer applies multiple criteria — delivery fee range, price bracket, and category — simultaneously, recomputing the filtered list through a single reactive computed property. The cart layer tracks items, quantities, and totals, including tax and delivery fee calculations that update in real time as the user adds or removes products.

What makes this architecture interesting is that none of the composables know about each other. The view component is the only place where they compose together. This means each composable can be tested in isolation with zero setup — pass in mock state, assert the output. No store module registration, no provider setup, no DI container.

composables/
├── useFilters.tsFilter state + multi-criteria logic
├── useCart.tsCart items, totals, tax, delivery
└── useProducts.tsProduct fetching + sorting
components/
├── ProductGrid.vueResponsive product cards
├── FilterBar.vueSidebar filter controls
└── CartSidebar.vueCart summary with real-time totals
Tip·

Why composables over Pinia?

For a focused application like this, Vue 3's Composition API composables provide clean state management without the boilerplate of a dedicated store. Each composable is a self-contained module with its own state, computed properties, and methods — testable in isolation, composable at the view level.

Multi-Criteria Filtering in Practice

The filtering system turned out to be more interesting than expected. Supporting price range AND delivery fee AND category filters simultaneously means the filtered product list needs to recompute on every change to any criterion. Using Vue 3's computed with proper reactive dependency tracking made this efficient — only the products that match all active criteria pass through.

The trickiest part was debouncing. When a user drags the price range slider, the filter fires on every pixel change. Without debouncing, the computed property runs dozens of times per second. Adding a lightweight debounce to the filter input — not the computed itself, but the upstream slider event — reduced unnecessary recomputation without introducing perceptible lag.

Responsive Grid

The product grid transitions between 1, 2, and 3 columns depending on viewport width. Tailwind's responsive utility classes handle most of this, but the real challenge was making sure the cart sidebar and filter bar behave correctly at each breakpoint. On mobile, the filter bar becomes a slide-over panel and the cart is always visible at the bottom. On desktop, all three panels sit side by side. Each breakpoint required rethinking the layout hierarchy, not just adjusting widths.

What I Learned

The biggest takeaway was that Composition API composables are a genuine alternative to Pinia or Vuex for applications of this scale. Each composable is just a function that returns reactive state and methods — no store setup, no module registration, no mutation types. The tradeoff is that cross-composable communication requires the view component to wire things together explicitly, which can feel verbose compared to a centralized store with getters and actions.

Multi-criteria filtering taught me to be careful with reactive dependencies. When you have four active filters, it's easy to accidentally create circular computed dependencies or trigger unnecessary recomputation. The solution was to keep each filter criterion as a separate ref and combine them in a single computed that depends on all of them — Vue's dependency tracking handles the invalidation correctly as long as you don't nest computed properties.

Tailwind CSS made the responsive layout genuinely productive. The grid goes from one column to three with a single class change, and the collapsible sidebar pattern worked well with state-driven classes rather than media query overrides.

TypeScript caught several filter logic errors at compile time. Defining the filter state as a typed interface meant that adding a new criterion — say, an "organic only" toggle — was type-safe from the start. The compiler flags any place where the new field is missing, which is exactly the kind of safety you want when refactoring state logic.


Built to explore Vue 3 Composition API patterns. View source · Live demo