Add Easel to Vite.
Easel expects a Shopify theme at the Vite root and source files in
src. JavaScript works in place of TypeScript.
Requires Node.js 22.12 or newer and Vite 7 or 8.
Starting a new theme? The Clack-based scaffolder configures Easel, Vite, and optional Alpine, React, Vue, or Tailwind. It is available from a repository checkout, but is not published to npm yet.
pnpm add -D vite-plugin-shopify-easel vite
import {defineConfig} from 'vite';
import easel from 'vite-plugin-shopify-easel';
export default defineConfig({
plugins: [easel()],
});
Add these scripts to your
package.json:
{
"scripts": {
"dev": "vite",
"build": "vite build"
}
}
{% render 'easel-assets' %}
.easel/
assets/easel-*
snippets/easel-assets.liquid
Zero configuration entry
Easel creates a bundle named
theme
from
src/main.ts
or
src/main.js, plus
src/style.css.
Two tools, one workflow.
Run Vite and Shopify CLI independently. Vite serves frontend assets while Shopify CLI synchronizes the theme.
npm run dev
shopify theme dev --notify .easel/shopify-ready
The notification lets Easel request a full page reload after Shopify CLI finishes an update. It is optional. Vite HMR and Shopify CLI live reload continue to work without it.
Build, then deploy normally.
Easel publishes generated assets into the theme and replaces the development loader with Shopify CDN asset tags.
npm run build
shopify theme push
Run the build before
shopify theme push
or
shopify theme package. Shopify CLI remains responsible for deployment.
Load code where it belongs.
Start with the default bundle. Add named bundles when a substantial feature should only load on selected Liquid surfaces.
easel({
bundles: {
theme: {script: 'main.ts', style: 'style.css'},
product: {script: 'product.ts', style: 'product.css'},
},
});
{% render 'easel-assets', entry: 'theme' %}
{% if request.page_type == 'product' %}
{% render 'easel-assets', entry: 'product' %}
{% endif %}
-
Use dynamic imports first
They keep one entry architecture while Vite loads optional features on demand.
-
Use bundles for clear boundaries
Product configurators, account areas, and store locators are good candidates.
Small API, explicit control.
Every Easel option is optional.
easel({
// Theme directory relative to the Vite root.
theme: '.',
// Source directory relative to the Vite root.
source: 'src',
// Namespace for generated assets and Liquid.
namespace: 'easel',
// Set to false to disable coordinated reloads.
refresh: {
signal: '.easel/shopify-ready',
delay: 100,
},
});
Bring the Vite ecosystem.
Easel does not wrap frontend frameworks. Add their official Vite plugins as usual.
-
Tailwind CSS
Official Vite plugin
-
Alpine.js
Standard entry import
-
React
Fast Refresh ready
-
Vue
Single file components
-
Sass
Native Vite support
-
PostCSS
Native Vite config
React Fast Refresh requires
@vitejs/plugin-react/preamble
because Easel does not use an HTML entry. Framework code mounted inside
sections should handle Shopify Theme Editor section load and unload events.
Built for a shared assets directory.
Shopify mixes generated assets and hand authored files in one directory. Easel cleans up without treating that directory like a disposable build folder.
-
Ownership ledger
Only files recorded as Easel output can be removed as stale.
-
Isolated staging
Every build finishes away from the theme before publication.
-
Protected writes
Unknown theme files are never silently overwritten.
-
Recovery journal
Interrupted publication is restored before the next build continues.
Troubleshooting
Find the first error in your terminal or browser, then follow the checklist below.
Missing styles or JavaScript
-
Run
npm run devandshopify theme devin separate terminals. Check the previewed theme. -
Render
{% render 'easel-assets' %}in the layout. Match any custom namespace; do not edit the generated snippet. - Connection refused? Check the dev server. A 404? Check the requested path. Requests succeed? Check JavaScript errors and CSS rules.
Check the setup or use the production checklist for a deployed theme.
CORS errors
- Easel allows myshopify.com, Shopify Admin, and localhost by default.
-
Custom
server.corssettings replace those defaults. Keep them in the list below. -
Replace
https://www.example.comwith the request's exactOrigin: scheme, hostname, and port, with no path. Add www and non-www separately if needed.
import {defineConfig} from 'vite';
import easel from 'vite-plugin-shopify-easel';
export default defineConfig({
plugins: [easel()],
server: {
cors: {
origin: [
/^https?:\/\/(?:[^.]+\.)*myshopify\.com$/,
'https://admin.shopify.com',
/^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/,
'https://www.example.com',
],
},
},
});
- Restart Vite after changing the config.
-
Never use
cors: true: it exposes your source to any website. CORS reference.
Browser and Theme Editor access
- Local-network error: check the site's browser permission. Grant access only to trusted sites.
- Certificate or mixed-content error: use trusted HTTPS. Do not bypass warnings or disable browser security.
-
Remote browser: its localhost is not your laptop. Set
up an external endpoint and set
server.originto its URL. Easel does not create tunnels. -
Host blocked: add the exact development hostname you
control to
server.allowedHosts, nevertrue. This differs from the storefront's CORS origin. - HMR disconnected: proxy WebSockets too. Check Vite's HMR settings.
- Public tunnel: restrict access and stop it when finished.
Changes do not reload
- JavaScript/CSS: check Vite's terminal and the HMR WebSocket.
- Liquid: check Shopify CLI for sync errors and confirm the previewed theme.
-
Coordinated reload: pass
--notify .easel/shopify-readyto Shopify CLI from Vite's project root. Match any customrefresh.signal;refresh: falsedisables it.
Coordination is optional. Vite HMR and Shopify CLI live reload work independently. Development commands.
Localhost in a published theme
- Stop Vite before building or deploying.
-
After building, check
snippets/easel-assets.liquidusesasset_url, not localhost,/@vite/client, or a tunnel. - Deploy the snippet and matching assets together. Check CLI ignore rules and upload flags, even though the snippet is Git-ignored.
Terminal · stop Vite first, then build and deploy
npm run build
shopify theme push
Protected writes and recovery
-
Name collision: preserve the existing file. Choose
another
namespaceand update the layout's render call. - Lock or interrupted build: stop competing Easel processes, then retry. Recovery is automatic; retain errors and files if it still fails.
-
Do not delete
assets/or.easel/to force a build. The ledger and journal support safe recovery.
No separate cleanup plugin is needed. Output safety.
Entry and configuration errors
-
Defaults:
src/main.tsorsrc/main.js, not both, plussrc/style.css. -
Custom entries: set
sourceandbundles. Each bundle needs a script, style, or both; script entries must be distinct. -
Wrong theme: check Vite's root and Easel's
themepath. -
Output conflicts: remove overrides such as
build.outDirandbuild.rollupOptions.output. Use Easel's configuration options instead.
Framework lifecycle issues
-
Mount on initial page load and
shopify:section:load; destroy or unmount onshopify:section:unload. -
Remove global listeners in
import.meta.hot.disposeto prevent duplicates. -
React needs
@vitejs/plugin-react/preamblebecause Easel has no Vite HTML entry.
Easel does not mount components for you. Copy the lifecycle patterns from the runnable examples.
Still stuck?
Requires Node.js 22.12+ and Vite 7 or 8. With pnpm, replace
npm ls below with pnpm list.
node --version
npm --version
npm ls vite vite-plugin-shopify-easel --depth=0
shopify version
- Open an issue with versions, OS/browser, commands, the exact error, and a minimal reproduction with redacted config.
- Say where it fails: CLI preview, Theme Editor, or production. For failed requests, include the status and origin.
- Remove tokens, cookies, authorization headers, and sensitive URL parameters. Do not upload raw network exports or recovery files.
See it in a real theme.
Every example is independently runnable and checked in CI. Connect one to your Shopify development store to try it.
-
Vanilla
TypeScript, CSS, dynamic imports, fonts, and images
-
Named bundles
Separate product and collection entries with TypeScript and CSS
-
Alpine and Tailwind
Alpine.js with Tailwind CSS through Vite
-
React
React components mounted inside Liquid sections
-
Vue
Vue single file components inside Liquid sections