Skip to content

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.

Install Easel
pnpm add -D vite-plugin-shopify-easel vite
vite.config.ts
import {defineConfig} from 'vite';
import easel from 'vite-plugin-shopify-easel';

export default defineConfig({
  plugins: [easel()],
});

Add these scripts to your package.json:

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}
layout/theme.liquid
{% render 'easel-assets' %}
Generated output ignores
.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.

Terminal one
npm run dev
Terminal two
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.

Build and push
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.

vite.config.ts
easel({
  bundles: {
    theme: {script: 'main.ts', style: 'style.css'},
    product: {script: 'product.ts', style: 'product.css'},
  },
});
layout/theme.liquid
{% 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.

vite.config.ts
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,
  },
});
theme
string

Shopify theme directory. Defaults to the Vite root.

source
string

Source directory. Defaults to src.

bundles
Record<string, EaselBundle>

Named script and stylesheet entry definitions. Each bundle needs a script, a style, or both.

bundles[name].script
string

Optional script entry, relative to source. When provided, script entries must be distinct across named bundles.

bundles[name].style
string

Optional stylesheet entry, relative to source.

namespace
string

Generated asset and snippet namespace. Defaults to easel.

refresh
boolean | EaselRefreshOptions

Notification file and debounce settings, or false. Enabled by default.

refresh.signal
string

Shopify CLI --notify file relative to the Vite root. Defaults to .easel/shopify-ready.

refresh.delay
number

Debounce delay in milliseconds. Defaults to 100; must be an integer from 0 to 10000.

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 dev and shopify theme dev in 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.cors settings replace those defaults. Keep them in the list below.
  • Replace https://www.example.com with the request's exact Origin: scheme, hostname, and port, with no path. Add www and non-www separately if needed.
vite.config.ts · custom storefront origin
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.origin to its URL. Easel does not create tunnels.
  • Host blocked: add the exact development hostname you control to server.allowedHosts, never true. 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-ready to Shopify CLI from Vite's project root. Match any custom refresh.signal; refresh: false disables 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.liquid uses asset_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 namespace and 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.ts or src/main.js, not both, plus src/style.css.
  • Custom entries: set source and bundles. Each bundle needs a script, style, or both; script entries must be distinct.
  • Wrong theme: check Vite's root and Easel's theme path.
  • Output conflicts: remove overrides such as build.outDir and build.rollupOptions.output. Use Easel's configuration options instead.

Framework lifecycle issues

  • Mount on initial page load and shopify:section:load; destroy or unmount on shopify:section:unload.
  • Remove global listeners in import.meta.hot.dispose to prevent duplicates.
  • React needs @vitejs/plugin-react/preamble because 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.

Terminal · installed versions
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