Setting Up Vite for Shopify Theme Development
Configure Vite to compile React components and output directly to your Shopify theme's assets folder. Simple setup with no intermediate build folders.
Vite is a modern build tool that will compile our React code and output it directly to the Shopify theme. In this lesson, we’ll configure Vite to build straight to shopify-theme/assets/—no intermediate folders, no copy steps.
Prerequisites
Before we begin, ensure you have:
- Node.js 18+ installed
- The folder structure from the previous lesson
- Basic familiarity with npm/yarn
Step 1: Initialize the Project
From your project root (not inside shopify-theme/), initialize npm:
npm init -yInstall the core dependencies:
# React and ReactDOMnpm install react react-dom
# Vite and React pluginnpm install -D vite @vitejs/plugin-react
# TypeScript supportnpm install -D typescript @types/react @types/react-domStep 2: Create the Vite Configuration
Create vite.config.ts in your project root:
import { defineConfig } from 'vite';import react from '@vitejs/plugin-react';import { resolve } from 'path';
/* * Vite config for Shopify themes * Key difference from standard React apps: we output directly to the theme folder * instead of a dist/ directory, so Shopify CLI can sync the files immediately */export default defineConfig({ plugins: [react()],
build: { // Output directly to shopify-theme/assets - no intermediate folders // This is what makes the Shopify CLI workflow seamless outDir: 'shopify-theme/assets',
// CRITICAL: Don't clear the folder! Other theme assets (images, fonts, // existing CSS) live here. Setting this to true would delete them. emptyOutDir: false,
// Enable source maps so you can debug original .tsx files in browser DevTools sourcemap: true,
rollupOptions: { // Single entry point - Vite will bundle everything this imports input: resolve(__dirname, 'src/main.tsx'), output: { // Fixed filename so Liquid can reference it reliably // (no hash = same URL on every build = easier caching management) entryFileNames: 'react-bundle.js',
// CSS extracted to a separate file for better caching assetFileNames: (assetInfo) => { if (assetInfo.name?.endsWith('.css')) { return 'react-bundle.css'; } return 'react-[name].[ext]'; },
// Disable code splitting - simpler for Shopify themes // One bundle loads on all pages, cached by the browser manualChunks: undefined, }, },
// Terser for smaller production bundles minify: 'terser', terserOptions: { compress: { // Remove console.log in production (saves ~2KB and cleans up DevTools) drop_console: process.env.NODE_ENV === 'production', }, }, },
resolve: { // Path aliases for cleaner imports: '@/components/Button' vs '../../components/Button' // Must match paths in tsconfig.json for TypeScript to understand them alias: { '@': resolve(__dirname, 'src'), '@components': resolve(__dirname, 'src/components'), '@hooks': resolve(__dirname, 'src/hooks'), '@stores': resolve(__dirname, 'src/stores'), '@utils': resolve(__dirname, 'src/utils'), '@types': resolve(__dirname, 'src/types'), }, },});Key settings:
outDir: 'shopify-theme/assets'— Vite outputs directly to your themeemptyOutDir: false— Preserves other assets (images, base CSS, fonts)- No copy plugin needed—files go straight where they belong
Step 3: Configure TypeScript
Create tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true,
"moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx",
"strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true,
"baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@hooks/*": ["src/hooks/*"], "@stores/*": ["src/stores/*"], "@utils/*": ["src/utils/*"], "@types/*": ["src/types/*"] } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }]}Create tsconfig.node.json for Vite’s Node.js context:
{ "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "strict": true }, "include": ["vite.config.ts"]}Step 4: Create the Source Directory Structure
Set up your src/ folder:
mkdir -p src/{components,hooks,stores,types,utils,styles}Create the entry point src/main.tsx:
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';
// Global styles are bundled into react-bundle.cssimport './styles/main.css';
/* * Component Registry Pattern * Maps DOM element IDs to React components. When the page loads, * we scan for these IDs and mount the corresponding components. * This allows Liquid to define mount points: <div id="product-form-root"></div> */const COMPONENTS: Record<string, React.LazyExoticComponent<React.ComponentType>> = { // Components will be added here as we build them // Format: 'dom-element-id': lazy(() => import('./path/to/Component')), // 'product-form-root': lazy(() => import('./components/product/ProductForm')),};
function mountComponents() { // For each registered component, find its mount point and render Object.entries(COMPONENTS).forEach(([elementId, Component]) => { const element = document.getElementById(elementId); if (element) { // createRoot is React 18's new API (replaces ReactDOM.render) const root = createRoot(element); root.render( // StrictMode helps catch common bugs during development // (double-renders components to detect side effects) <StrictMode> <Component /> </StrictMode> ); } });}
// Handle both cases: script loaded before or after DOM is readyif (document.readyState === 'loading') { // DOM not ready yet - wait for it document.addEventListener('DOMContentLoaded', mountComponents);} else { // DOM already ready (script was deferred or loaded late) mountComponents();}
// Development-only logging (stripped in production build)if (import.meta.env.DEV) { console.log('🚀 React bundle initialized');}Create a basic styles file src/styles/main.css:
/* * React component styles * These are bundled into react-bundle.css */
/* CSS Variables (can be overridden by Liquid/theme settings) */:root { --react-transition-fast: 150ms; --react-transition-normal: 300ms; --react-transition-slow: 500ms;}
/* Base component resets */.react-component { box-sizing: border-box;}
.react-component *,.react-component *::before,.react-component *::after { box-sizing: inherit;}Step 5: Create the Shopify Theme Structure
If you don’t have an existing theme, create the basic structure:
mkdir -p shopify-theme/{assets,config,layout,locales,sections,snippets,templates}Create a minimal shopify-theme/layout/theme.liquid:
<!DOCTYPE html><html lang="{{ shop.locale }}"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{%- comment -%} React CSS {%- endcomment -%} {{ 'react-bundle.css' | asset_url | stylesheet_tag }}
{{ content_for_header }}</head><body> {{ content_for_layout }}
{%- comment -%} React JS - loaded at end of body {%- endcomment -%} <script src="{{ 'react-bundle.js' | asset_url }}" defer></script></body></html>Step 6: Add npm Scripts
Update your package.json:
{ "name": "shopify-react-theme", "private": true, "type": "module", "scripts": { "dev": "vite build --watch --mode development", "build": "tsc --noEmit && vite build", "typecheck": "tsc --noEmit", "typecheck:watch": "tsc --noEmit --watch" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", "typescript": "^5.3.0", "vite": "^5.0.0" }}Step 7: Test the Build
Run the development build:
npm run devYou should see output like:
vite v5.0.0 building for development...✓ 2 modules transformed.shopify-theme/assets/react-bundle.js 45.12 kB │ gzip: 14.89 kBshopify-theme/assets/react-bundle.css 0.15 kB │ gzip: 0.10 kB✓ built in 234mswatching for file changes...Check that the files exist:
ls -la shopify-theme/assets/# react-bundle.js, react-bundle.cssThat’s it! No intermediate folders, no copy steps. Files go directly where Shopify needs them.
Advanced Configuration
Environment Variables
Create environment files:
VITE_DEBUG=true
# .env.productionVITE_DEBUG=falseAccess in your React code:
if (import.meta.env.VITE_DEBUG === 'true') { console.log('Debug mode enabled');}Multiple Entry Points (Advanced)
For large themes, you might want separate bundles per page type:
/* * Multiple Entry Points: Split code by page type * Useful for large themes where you don't want to load all React code on every page * Trade-off: More files to manage, but smaller per-page bundles */export default defineConfig({ build: { outDir: 'shopify-theme/assets', emptyOutDir: false, rollupOptions: { // Each entry creates a separate bundle input: { main: resolve(__dirname, 'src/main.tsx'), // Shared components (header, cart) product: resolve(__dirname, 'src/entries/product.tsx'), // Product page only collection: resolve(__dirname, 'src/entries/collection.tsx'), // Collection page only }, output: { // [name] is replaced with the key from input (main, product, collection) entryFileNames: 'react-[name].js', assetFileNames: 'react-[name].[ext]', }, }, },});Then load conditionally in Liquid:
{%- if template.name == 'product' -%} <script src="{{ 'react-product.js' | asset_url }}" defer></script>{%- endif -%}CSS Modules
Vite supports CSS Modules out of the box:
.button { padding: 1rem 2rem; background: var(--color-primary);}
.button:hover { opacity: 0.9;}import styles from './Button.module.css';
export function Button({ children }) { return <button className={styles.button}>{children}</button>;}Production Optimization
For production builds, add these optimizations:
/* * Production Optimization Settings * Shopify themes benefit from smaller bundles since assets are served via CDN */export default defineConfig({ build: { // Assets smaller than 4KB are inlined as base64 // Reduces HTTP requests for tiny icons/images assetsInlineLimit: 4096,
// Warn if any chunk exceeds 50KB (Shopify best practice) // Large bundles = slower initial page load chunkSizeWarningLimit: 50,
rollupOptions: { output: { // Remove whitespace and comments for smaller output compact: true, }, }, },
esbuild: { // Strip console.log and debugger statements in production // Saves bytes and keeps DevTools clean for store visitors drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [], },});Troubleshooting
”Module not found” errors
Ensure your tsconfig.json paths match vite.config.ts aliases:
resolve: { alias: { '@': resolve(__dirname, 'src'), },}"paths": { "@/*": ["src/*"]}Other assets getting deleted
Make sure emptyOutDir is set to false:
build: { outDir: 'shopify-theme/assets', emptyOutDir: false, // IMPORTANT!}TypeScript errors not blocking build
Run typecheck separately:
npm run typecheck # Fails on type errorsnpm run build # Runs typecheck first via tsc --noEmitShopify not serving the new bundle
Shopify aggressively caches assets. Try:
- Hard refresh:
Cmd+Shift+R(Mac) orCtrl+Shift+R(Windows) - Wait a few minutes for CDN propagation
- Check that Shopify CLI is syncing the changed files
Complete File Checklist
After this lesson, you should have:
your-project/├── shopify-theme/│ ├── assets/│ │ ├── react-bundle.js ✓ (generated by Vite)│ │ └── react-bundle.css ✓ (generated by Vite)│ └── layout/│ └── theme.liquid ✓├── src/│ ├── styles/│ │ └── main.css ✓│ └── main.tsx ✓├── package.json ✓├── tsconfig.json ✓├── tsconfig.node.json ✓└── vite.config.ts ✓Key Takeaways
- Vite outputs directly to
shopify-theme/assets/—no intermediate folders - Use
emptyOutDir: falseto preserve other theme assets - Path aliases keep imports clean and maintainable
- Watch mode rebuilds on every save for rapid iteration
- Simple is better—fewer moving parts means fewer things to break
In the next lesson, we’ll cover Webpack configuration as an alternative approach.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...