Skip to content

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:

Create in the current 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
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.

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.

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.

Use one terminal (optional)

A process runner can start both tools without making Easel responsible for Shopify CLI. Install concurrently:

Install the process runner
npm install --save-dev concurrently

Keep the independent commands and add a third script that supervises both:

package.json
{
  "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.

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.

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.

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