Setting the file. One moment.
Product Listing · Storefront Best Practices · medusajs/medusa-agent-skills · Skills Docs
ContentsBack to the top of the page Mcloud
Primary browsing interface where users compare products, apply filters, and navigate to product details. Critical for product discovery and conversion.
Responsive product grid (3-4 columns desktop, 2 mobile)
Filtering (categories, price, attributes)
Sorting options (price, popularity, newest)
Pagination, infinite scroll, or load more
Results count and active filter indicators
Clear “no results” state with suggestions
Fast loading and filtering (<1s filter updates)
Backend integration for dynamic filtering
Build product listing as a reusable component that works across multiple pages:
✅ Use the same product listing component for:
“Shop All” page (all products, no category filter)
Category pages (filtered by specific category)
Search results page (filtered by search query)
Sale/Promotion pages (filtered by discount/promotion)
Collection pages (curated product sets)
Brand pages (filtered by brand)
Benefits of reusable approach:
Single source of truth for product browsing UI
Consistent filtering, sorting, and pagination behavior across entire site
Easier maintenance (fix bugs once, applies everywhere)
Better user experience (familiar interface on every product browsing page)
Significantly less code duplication
What to make configurable:
Initial filter parameters (category ID, search query, promotion ID, brand, etc.)
Page title and breadcrumbs
Whether to show filters sidebar (some pages may hide certain filters)
Default sort order (category: featured, search: relevance, sale: discount %)
Number of products per page
Filter options available (hide category filter on category pages, etc.)
❌ Creating separate components/pages for “Shop All”, category pages, and search results with duplicated filtering/sorting/pagination logic
✅ Build one reusable ProductListing component that accepts filter parameters and reuse it across all product browsing pages
CRITICAL: Always use dynamic routes for category pages, NEVER static pages.
Category/listing pages must use dynamic routes that accept a parameter (handle, slug, or category ID):
Next.js App Router: app/categories/[handle]/page.tsx
Next.js Pages Router: pages/categories/[handle].tsx
SvelteKit: routes/categories/[handle]/+page.svelte
TanStack Start: routes/categories/$handle.tsx
Remix: routes/categories.$handle.tsx
❌ pages/categories/women.tsx (static file per category)
❌ pages/categories/men.tsx (doesn’t scale)
Fetch category products in the dynamic route based on the handle/ID parameter from the URL.
This is a critical ecommerce decision that affects user experience, SEO, and technical implementation.
Return to specific result pages
Precise control over browsing
Professional/research shopping (compare systematically)
B2B shoppers (procurement, large orders)
Position matters (rankings, bestsellers)
Large catalog with stable ordering
Products require careful comparison
SEO-friendly (unique URL per page)
Better for indexing and crawling
Easier back button support
Lower memory usage
Desktop-heavy audience
B2B ecommerce
Product comparison shopping
Catalog with 100+ products
Exploratory browsing behavior
Mobile-first experience
Seamless discovery flow
Fashion/visual shopping
Visual-heavy products (fashion, art, photography)
Impulse purchases
Discovery-focused (Pinterest-style)
Technical considerations:
More complex to implement
Requires careful SEO handling (pagination URLs still needed)
Higher memory usage (all loaded products stay in DOM)
Need to handle browser back button carefully
Mobile-first stores (>60% mobile traffic)
Fashion, home decor, visual products
Younger demographic (18-34)
Discovery-focused shopping
User controls when to load (not automatic)
Footer remains accessible (important for policies, contact)
Better for slower connections (international users)
Accessibility friendly (no automatic loading)
Lower memory usage than infinite scroll
International audience (varying connection speeds)
Footer content is important (legal, policies, contact)
Accessibility concerns with infinite scroll
Compromise between pagination and infinite scroll
Combine patterns based on context:
Pagination for SEO (canonical URLs)
Infinite scroll for UX (on user interaction)
Load more for control (user-triggered)
Many filter options (5+ categories)
Complex product attributes
Power users (B2B, professional shoppers)
Desktop-heavy traffic
Left sidebar (250-320px wide)
Sticky position (scrolls with page)
Collapsible sections (accordion)
Apply immediately (no “Apply” button)
Few filter options (2-4 key filters)
Maximize grid space (full-width layout)
Simple product categories
Visual-first products (fashion)
Horizontal filter bar above grid
Dropdowns or button toggles
Limited options (price, category, brand)
Compact design
“Filters” button at top (shows active count badge)
Slide-out drawer (full-screen or 80% width)
Accordion sections
“Apply” button at bottom (batch filtering)
“Clear All” option
Why batch filtering on mobile:
Prevents multiple re-renders on slow connections
User can adjust multiple filters before applying
Better mobile UX (less disruptive)
Large desktop (>1440px): 4 columns
Desktop (1024-1440px): 3-4 columns
Tablet (768-1024px): 3 columns
Mobile (< 768px): 2 columns
Adjust based on product type:
Fashion/lifestyle: 3-4 columns (more visible at once)
Electronics/detailed: 2-3 columns (larger cards, more detail)
Furniture/large items: 2-3 columns (showcase details)
Product image (primary)
Title (truncated to 2 lines)
Price (Medusa: display as-is, don’t divide by 100)
Optional: Rating, badges, wishlist
See product-card.md for detailed guidelines
16-24px gap (desktop)
12-16px gap (mobile)
Equal height rows (optional, improves visual consistency)
Multi-select checkboxes
Hierarchical (parent-child categories)
Show product count per category
Example: “Shirts (24)” “T-Shirts (12)”
Range slider (drag min/max)
Or: Predefined ranges (“$0-$50”, “$50-$100”)
Update dynamically as products filtered
Show min/max from current results
Attribute filters (Size, Color, Brand):
Multi-select checkboxes
Visual swatches for colors
Show available options based on current filters
Gray out unavailable combinations
“In Stock” checkbox
“On Sale” checkbox
“New Arrivals” checkbox
Single purpose, clear value
Save in URL parameters (shareable, bookmarkable)
Example: /products?category=shirts&price=0-50&color=blue
Restore filters on page reload
Clear all filters should reset URL
Above product grid
Pill/tag format: “Blue ✕” “Under $50 ✕”
Click X to remove individual filter
“Clear All” link to remove all filters
Count: “3 filters active”
Featured (default): Store’s recommended order (bestsellers, promoted)
Price: Low to High : Budget-conscious shoppers
Price: High to Low : Premium product seekers
Newest : Fashion, tech, time-sensitive products
Best Selling : Social proof, popular choices
Top Rated : Quality-focused shoppers
Name: A-Z (alphabetical)
Discount: Highest % off (sale hunters)
Reviews: Most reviewed (validation seekers)
Dropdown above product grid (right-aligned)
Label: “Sort by:” or just dropdown
Update products immediately on selection
Show current sort in URL: /products?order=-created_at
Pass sort parameter to API (check backend docs for parameter name)
Common parameters: order, sort, sort_by
Common values: -created_at (desc), +price (asc), -price (desc)
Sorting doesn’t clear filters
Maintains all active filters
Updates URL with sort parameter
Query parameters to include:
Category/collection filter (if applicable)
Pagination (limit, offset or cursor)
Sort order
Filter values (price, attributes, etc.)
For Medusa: region_id (required for correct pricing)
Check backend API documentation for exact parameter names and formats.
Show only relevant filters for current category
Display product count per filter option
Gray out options with 0 products
Update available options when filters change
Filter URL structure pattern:
/products?category_id=123&order=-created_at&page=2&price=0-50
Shareable links
Bookmarkable searches
Browser back/forward works correctly
SEO-friendly (crawlable filter combinations)
Read filters from URL query parameters on page load
Update URL when filters change using URLSearchParams and history.pushState
Parse URL parameters to reconstruct filter state
Message: “No products available yet”
Subtext: “Check back soon for new arrivals”
CTA: “Browse all products” or “Go to home”
Alternative: Show related categories
Optional: Newsletter signup for notifications
When filters too restrictive:
Message: “No products match your filters”
Subtext: “Try removing some filters or adjusting your criteria”
Prominent “Clear All Filters” button
Show which filters might be too restrictive
Suggestions: “Try expanding price range” or “Remove brand filter”
When search query returns nothing:
Message: “No results for ‘[query]’”
Suggestions: Check spelling, try different keywords
CTA: Browse popular categories
Show search suggestions (similar queries)
Display popular or trending products
Load images as they come into viewport
Use Intersection Observer API
Show placeholder or blur-up effect
Improves initial page load significantly
Product listings have 24-100+ images per page
Lazy loading reduces initial load by 60-80%
Faster perceived performance
Very large catalogs (500+ products visible)
Infinite scroll with memory concerns
Performance issues with many DOM elements
Only render visible products + buffer
Reuse DOM elements as user scrolls
Maintains scroll position
Libraries: react-window, react-virtuoso
Complex implementation
Better performance for large lists
Required for catalogs with 1000+ products loaded
Update grid immediately (predicted results)
Show loading overlay briefly
Replace with real results
Better perceived performance
Critical mobile patterns:
Maximum 2 products per row
Larger touch targets
Simplified cards (essential info only)
Remove hover effects
Full-screen or 80% width drawer
“Filters” button with badge count
Batch apply (don’t re-fetch on each change)
Clear all at top
Fixed at top while scrolling
Quick access to filters and sorting
Shows active filter count
Higher engagement rates
Better mobile UX than pagination
Natural scrolling behavior
Keep pagination URLs for SEO
Handle back button correctly
Lazy load images (critical on mobile)
Limit initial products (12-24)
Optimize image sizes for mobile
Fast filter updates (<1s)
Essential product listing features:
Not done: RECOMMENDED: Product listing built as reusable component
Not done: Reusable component works for: shop all, category pages, search results, sale pages
Not done: Component accepts filter parameters (categoryId, searchQuery, promotionId, etc.)
Not done: Responsive grid (3-4 columns desktop, 2 mobile)
Not done: Decision made: Pagination vs infinite scroll vs load more
Not done: Filter pattern selected: Sidebar (desktop) vs drawer (mobile)
Not done: Filters fetched from backend dynamically
Not done: Filter options show product count
Not done: Active filters displayed above grid (removable pills)
Not done: “Clear all filters” button prominent
Not done: Sorting options (featured, price, newest, bestselling)
Not done: Sort updates products without clearing filters
Not done: Filters and sort persist in URL (shareable)
Not done: Results count displayed (“Showing 1-24 of 156 products”)
Not done: Empty state: “No products match filters” with suggestions
Not done: “Clear all filters” prominent when no results
Not done: Product prices displayed correctly (Medusa: as-is, not divided)
Not done: Lazy loading for images (Intersection Observer)
Not done: Loading state for filter changes (< 1s)
Not done: Mobile: Filter drawer with batch apply
Not done: Mobile: 2-column grid maximum
Not done: Mobile: Sticky filter/sort button
Not done: Pagination URLs for SEO (even with infinite scroll)
Not done: Back button support (restore filters, scroll position)
Not done: Keyboard accessible (tab through filters, enter to apply)
Not done: ARIA labels on filters (role=”group”, aria-label)