11 Features Every Shopify Store Needs (That Templates Can't Do)
Not a wish list. Every feature here is built, running in production, and toggleable from a single config file. Here is what each one does and why Shopify templates cannot replicate it.
Shopify templates do a lot. Theme developers are talented. The Liquid templating language is more capable than most people give it credit for. But there is a category of features that requires JavaScript control, build-time data access, and component architecture that Shopify's theme system does not support.
These 11 features are not theoretical. Every one is implemented in our headless storefront platform, running in production, and controllable from a single boolean in the config file. I will explain what each one does, show the implementation, and explain why a Shopify template cannot do the same thing.
1. Cmd+K Product Search
What it is: A keyboard-activated search modal. Press Cmd+K (or Ctrl+K on Windows) and a full-screen search overlay appears. Type a query and see instant results with product images, prices, and availability. Arrow keys to navigate. Enter to go to the product page. Escape to close.
Why templates cannot do it: Shopify themes can add a search bar that submits to /search?q=term. That triggers a full page load. The results page is a Liquid template with limited control over ranking and display. You cannot do instant, client-side filtering because themes do not have access to the full product catalog as JSON at runtime without an additional API call per keystroke.
The implementation:
function SearchModal({ products }: { products: Product[] }) {
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const results = useMemo(() => {
if (!query.trim()) return [];
return products
.map(p => ({ product: p, score: scoreMatch(query, p) }))
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 8);
}, [query, products]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setOpen(true);
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
// Full keyboard navigation, image previews, instant results
}
The search runs against the full catalog that was fetched at build time. Zero API calls. Zero latency. Results appear as the user types. The scoring function weights title matches higher than tag matches, which rank higher than description matches. It feels like Spotlight or VS Code's command palette.
2. Product Image Zoom
What it is: Hover over a product image and a magnified view appears. On desktop, the zoom follows the cursor. On mobile, pinch-to-zoom with smooth gesture handling. The zoom renders at the full resolution of the Shopify CDN image (typically 2048x2048).
Why templates cannot do it: Shopify themes can use JavaScript libraries for zoom, but they compete with Shopify's own JavaScript for event handling. The performance is poor because Shopify's theme JavaScript is already consuming the CPU budget. In a headless storefront, image zoom is the only JavaScript running on the page.
The implementation: A CSS transform on a high-resolution image, positioned based on cursor coordinates. No library. About 60 lines of code. The key is loading the full-resolution image only when zoom is activated, so the initial page load uses the optimized srcset.
function ImageZoom({ src, alt }: { src: string; alt: string }) {
const [zoomed, setZoomed] = useState(false);
const [position, setPosition] = useState({ x: 50, y: 50 });
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseMove = (e: React.MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
setPosition({
x: ((e.clientX - rect.left) / rect.width) * 100,
y: ((e.clientY - rect.top) / rect.height) * 100,
});
};
// Full-res image loaded on hover, transform-origin follows cursor
return (
<div
ref={containerRef}
onMouseEnter={() => setZoomed(true)}
onMouseLeave={() => setZoomed(false)}
onMouseMove={handleMouseMove}
className="image-zoom-container"
>
<img src={src} alt={alt} />
{zoomed && (
<div
className="image-zoom-lens"
style={{
backgroundImage: `url(${src.replace('_800x', '_2048x')})`,
backgroundPosition: `${position.x}% ${position.y}%`,
}}
/>
)}
</div>
);
}
3. Quick View Modals
What it is: Click a "Quick View" button on any product card in a collection or search result. A modal appears with the product image, title, price, variant selector, and Add to Cart button. The customer can add to cart without leaving the collection page.
Why templates cannot do it: Shopify themes can build modals, but populating them with product data requires either pre-rendering every product's modal into the page HTML (which destroys performance for large collections) or making an AJAX call to fetch product data (which adds latency). In a headless storefront, the full product data is already in memory because it was fetched at build time.
The implementation: A React portal that renders over the collection page. The product data is passed as a prop. No fetch. No loading state. Instant.
4. Wishlist with Email Gate
What it is: A heart icon on every product card and product page. Click it to save the product. First save triggers an email capture modal. Subsequent saves are instant. Wishlist persists across sessions via localStorage. A dedicated /wishlist page shows all saved products.
Why templates cannot do it: Wishlist functionality in Shopify requires either a paid app ($10-30/month) or customer accounts (which require login). Our implementation works for anonymous visitors. The email capture on first save gives the merchant a lead without requiring the customer to create an account.
The implementation: React context with localStorage persistence. The email gate is a one-time modal that writes the email to the merchant's marketing platform (HubSpot, Klaviyo, or a custom endpoint configured in the config file).
function WishlistButton({ product }: { product: Product }) {
const { items, addItem, removeItem, hasEmail } = useWishlist();
const [showEmailGate, setShowEmailGate] = useState(false);
const isSaved = items.some(i => i.id === product.id);
const handleClick = () => {
if (isSaved) {
removeItem(product.id);
return;
}
if (!hasEmail) {
setShowEmailGate(true);
return;
}
addItem(product);
};
return (
<>
<button onClick={handleClick} aria-label={isSaved ? 'Remove from wishlist' : 'Add to wishlist'}>
<HeartIcon filled={isSaved} />
</button>
{showEmailGate && (
<EmailGateModal
onSubmit={(email) => {
captureEmail(email);
addItem(product);
setShowEmailGate(false);
}}
onClose={() => setShowEmailGate(false)}
/>
)}
</>
);
}
5. Dynamic Collection Filters
What it is: Sidebar filters on collection pages derived from product data. Price range slider. Checkbox filters for options (Color, Size, Material). Tag-based filters. URL updates with filter state so filtered views are shareable. Active filter count shown on mobile filter toggle.
Why templates cannot do it: Shopify's Liquid language added filtering in Online Store 2.0, but it is limited to Shopify's predefined filter types. You cannot create custom filter logic, combine filters with AND/OR operators, or derive filter options from arbitrary product data. The headless approach parses the actual product data and generates filters dynamically.
The implementation: Filters are extracted from product options and tags at build time. Filter state is stored in URL search parameters. The product grid re-renders client-side when filters change. No page reload. No API call. Sub-10ms filter operations because the data is already in memory.
6. Sticky Mobile CTA
What it is: On mobile product pages, a fixed bar appears at the bottom of the screen when the primary Add to Cart button scrolls out of view. Shows the product price and a compact Add to Cart button. Disappears when the primary button scrolls back into view.
Why templates cannot do it: This requires an IntersectionObserver watching the primary button. Shopify themes can technically implement this, but it conflicts with Shopify's own sticky header behavior and mobile navigation. The JavaScript execution timing is unpredictable because it runs alongside Shopify's framework JavaScript. In a headless storefront, there is no competing JavaScript. The IntersectionObserver runs reliably.
The implementation: An IntersectionObserver on the primary button, a fixed-position bar that conditionally renders, and smooth show/hide animations. About 40 lines of code. The impact is significant: mobile users always have a buy button within thumb reach.
7. Email Popup with Exit Intent
What it is: A modal that appears once per visitor when they move their cursor toward the browser's close button (exit intent on desktop) or after 30 seconds of browsing (on mobile). Offers a discount code or early access in exchange for an email. Stores dismissal state in localStorage. Never shows twice.
Why templates cannot do it well: Shopify themes rely on apps like Privy or Justuno for this. Each app injects 50-150KB of JavaScript. The popup loads after the page, causing layout shift. In a headless storefront, the popup is a native React component. It loads with the page. It is 3KB. Zero layout shift.
function EmailPopup() {
const [shown, setShown] = useState(false);
const dismissed = useRef(localStorage.getItem('popup-dismissed') === 'true');
useEffect(() => {
if (dismissed.current) return;
// Desktop: exit intent
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 0) setShown(true);
};
// Mobile: 30-second timer
const timer = setTimeout(() => {
if (!dismissed.current) setShown(true);
}, 30000);
document.addEventListener('mouseleave', handleMouseLeave);
return () => {
document.removeEventListener('mouseleave', handleMouseLeave);
clearTimeout(timer);
};
}, []);
const handleDismiss = () => {
setShown(false);
localStorage.setItem('popup-dismissed', 'true');
dismissed.current = true;
};
if (!shown) return null;
return <PopupModal onClose={handleDismiss} />;
}
8. Shipping and Returns Accordion
What it is: An expandable section on every product page with three tabs: Shipping, Returns, and Care Instructions. Content is defined in the config file. The first tab is open by default. Accessible with keyboard navigation and proper ARIA attributes.
Why templates cannot do it well: Shopify themes can add static content blocks, but they cannot pull from a centralized configuration. Each product page needs the content duplicated or pulled from metafields. Updating the return policy means updating every product's metafield or editing a theme section. In our system, the content lives in the config file. Change it once, it updates everywhere.
9. Availability Badges
What it is: Color-coded badges on product cards and product pages that show inventory status. Green for "In Stock," yellow for "Limited Stock" (under 15 units), red for "Only X Left" (under 5 units), gray for "Sold Out." The badge updates when the customer selects a different variant.
Why templates cannot do it: Shopify Liquid can access inventory data, but only at page render time. If a customer changes the variant selector, the badge does not update without JavaScript that fetches variant data via AJAX. In a headless storefront, all variant data is already loaded. The badge updates on variant change with zero network requests.
10. Social Sharing
What it is: Share buttons on product pages for Twitter/X, Facebook, Pinterest, and a copy-link button. Each button generates a platform-specific share URL with the product title, image, and link. The copy button shows a brief "Copied!" confirmation. Native Web Share API on mobile if available.
Why templates cannot do it well: Most Shopify themes use a share button app or a basic JavaScript snippet. These typically do not include Pinterest (which requires an image URL parameter) or the Web Share API fallback. They also inject additional tracking scripts. Our implementation is 50 lines of React with zero external dependencies.
function ShareButtons({ product }: { product: Product }) {
const url = `${config.store.domain}/products/${product.handle}`;
const text = `Check out ${product.title}`;
const image = product.images[0]?.src;
const share = async () => {
if (navigator.share) {
await navigator.share({ title: product.title, text, url });
return;
}
// Fallback to copy
await navigator.clipboard.writeText(url);
};
return (
<div className="share-buttons">
<a href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`}
target="_blank" rel="noopener">Twitter</a>
<a href={`https://pinterest.com/pin/create/button/?url=${encodeURIComponent(url)}&media=${encodeURIComponent(image)}&description=${encodeURIComponent(text)}`}
target="_blank" rel="noopener">Pinterest</a>
<button onClick={share}>Share</button>
</div>
);
}
11. Product Recommendations
What it is: A "You Might Also Like" section on every product page showing 4 related products. Recommendations are based on shared collections, shared tags, similar price range, and the same product type. Products are scored and ranked. The customer sees the 4 most relevant alternatives.
Why templates cannot do it: Shopify has a Recommendations API, but it requires the storefront to be on the Online Store channel and returns generic recommendations based on purchase history. It does not let you control the scoring algorithm. Our implementation uses attribute-based scoring with configurable weights.
function getRecommendations(
current: Product,
allProducts: Product[],
count: number = 4
): Product[] {
return allProducts
.filter(p => p.id !== current.id)
.map(p => ({
product: p,
score: calculateRelevance(current, p),
}))
.sort((a, b) => b.score - a.score)
.slice(0, count)
.map(r => r.product);
}
function calculateRelevance(a: Product, b: Product): number {
let score = 0;
// Same product type: strong signal
if (a.productType === b.productType) score += 5;
// Shared tags
const sharedTags = a.tags.filter(t => b.tags.includes(t));
score += sharedTags.length * 2;
// Similar price (within 30%)
const priceA = parseFloat(a.variants[0].price);
const priceB = parseFloat(b.variants[0].price);
const priceDiff = Math.abs(priceA - priceB) / priceA;
if (priceDiff < 0.3) score += 3;
// Same vendor
if (a.vendor === b.vendor) score += 2;
return score;
}
The Template Ceiling
Shopify templates hit a ceiling. That ceiling is the gap between what HTML/CSS/Liquid can do and what a modern JavaScript framework with build-time data access can do. Every feature on this list sits above that ceiling.
This is not a criticism of Shopify. Shopify is excellent at product management, order fulfillment, payment processing, and checkout. But the frontend is a different problem. It requires different tools.
All 11 features are controlled by boolean flags in a single config file. Turn them on. Turn them off. Deploy. The underlying components are built, tested, and running in production. The merchant does not implement them. They enable them.
Next up: how we added an AI assistant that knows every product in the catalog.