Start with Easel.
Easel requires Node.js 22.12 or newer. Generated themes use Vite 8; the plugin supports Vite 7 and 8.
Create a new theme
Run the published create-easel-theme scaffolder from an empty project directory:
npm create easel-theme@latest .
- Language: TypeScript or JavaScript.
- UI tools: none, Alpine, React, or Vue.
- Styling: plain CSS or Tailwind CSS.
- Install: npm, pnpm, Yarn, or Bun.
Creates an Easel/Vite starter with your selected integrations and storefront
templates. Run
npm run check:all
for source, build, and Theme Check validation. Shopify CLI remains separate.
Add Easel to an existing theme
Keep the Shopify theme at the Vite root and source files in
src. JavaScript works in place of TypeScript.
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.
Choosing Easel.
Easel is for developers building or modernizing a standard Shopify Liquid theme who want Vite without giving up Shopify's normal theme structure or CLI workflow.
A good fit
- You want TypeScript, modern CSS, HMR, or standard Vite plugins.
- You need safe ownership of generated files in Shopify's shared asset directory.
- You want one shared entry or selective script, style, and CSS-only bundles.
- You want vanilla code, Alpine, React, Vue, or Tailwind without a prescribed frontend architecture.
- You want Shopify CLI to remain responsible for stores, previews, synchronization, and deployment.
Choose another approach when
- You are building a Hydrogen or other custom storefront rather than a Liquid theme.
- You are building a Shopify app or UI extension.
- You need a tool to authenticate, select stores, or deploy without Shopify CLI.
- You do not need a Node.js or frontend build step.
- You want a complete production theme design rather than a focused development foundation.
The generator is intentionally small. It creates valid generic storefront templates and demonstrates lifecycle-safe integrations, but leaves product requirements, visual design, content modeling, and deployment policy to the theme project.
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.
Use one terminal (optional)
A process runner can start both tools without making Easel responsible for Shopify CLI. Install concurrently:
npm install --save-dev concurrently
Keep the independent commands and add a third script that supervises both:
{
"scripts": {
"dev": "vite",
"dev:shopify": "shopify theme dev --notify .easel/shopify-ready",
"dev:theme": "concurrently --kill-others --names vite,shopify \"npm:dev\" \"npm:dev:shopify\""
}
}
Run
npm run dev:theme
to see prefixed Vite and Shopify output together. Pressing
Ctrl+C
stops the group. Run Shopify CLI directly for initial authentication or
interactive troubleshooting; the original
dev
and
dev:shopify
scripts remain independently usable.
Build, then deploy normally.
Easel publishes generated assets into the theme and replaces the development
loader with Shopify CDN asset tags. Entry scripts use
type="module"; browsers defer modules automatically, so Easel does not emit a redundant
defer
attribute.
npm run build
shopify theme push
Run the build before
shopify theme push
or
shopify theme package. Shopify CLI remains responsible for deployment.
Render the loader inside
<head>. Shopify automatically preloads render-blocking stylesheets in
<head>
through
Link
headers, so the generated stylesheet tags don't need
preload: true.
Generated output is ignored by Git, so anything that deploys straight from
the repository, such as the Shopify GitHub integration, gets no loader or
assets. Build in CI and deploy with Shopify CLI, or commit the generated
output on the branch Shopify deploys. Run Theme Check after the build, since
layouts that render
easel-assets
report
MissingTemplate
until it exists.
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