| name | setup |
| description | Getting-started journey and plugin configuration. Covers the full path from install to first working island. Shopify-first setup: shopifyThemeIslands() options include directories (string | string[]), tagSource ("registeredTag" default — Tag derived from static customElements.define("...", ...) call; "filename" for v1.x compatibility), resolveTag({ filePath, defaultTag }) with unique final-tag requirements (defaultTag is the Registered Tag in registeredTag mode, filename-derived in filename mode), debug, directives deep-merge (visible, idle, media, defer, interaction, custom), retry (retries, delay with exponential backoff), directiveTimeout for hung custom directives, and the curated interaction-event config policy (`mouseenter`, `touchstart`, `focusin`; empty arrays rejected). Per-element `client:interaction` values are runtime-validated against the same curated set: unsupported tokens warn and are ignored; if no supported tokens remain, the runtime falls back to the configured default events.
|
| metadata | {"type":"core","library":"vite-plugin-shopify-theme-islands","library_version":"2.0.1"} |
| sources | ["Rees1993/vite-plugin-shopify-theme-islands:src/index.ts","Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts","Rees1993/vite-plugin-shopify-theme-islands:src/options.ts","Rees1993/vite-plugin-shopify-theme-islands:src/resolved-config.ts","Rees1993/vite-plugin-shopify-theme-islands:src/revive-compile.ts","Rees1993/vite-plugin-shopify-theme-islands:src/revive-module.ts","Rees1993/vite-plugin-shopify-theme-islands:src/interaction-events.ts"] |
Setup
This plugin is Shopify-first and built for Liquid themes using custom elements.
Most Shopify projects also use
vite-plugin-shopify to handle
Shopify-specific asset serving — if the project uses it, add this plugin
alongside it in the existing plugins array.
The package targets Node.js 22+ and declares Vite 6+ as a peer dependency.
1. Add the plugin to vite.config.ts
import { defineConfig } from "vite";
import shopifyThemeIslands from "vite-plugin-shopify-theme-islands";
export default defineConfig({
plugins: [shopifyThemeIslands()],
});
All options are optional. The default islands directory is /frontend/js/islands/.
2. Import the virtual module in the theme JS entry point
import "vite-plugin-shopify-theme-islands/revive";
This activates the runtime — islands are never loaded without this import.
The same /revive module also exports scan(), observe(), unobserve(),
and disconnect() for partial swaps and teardown. If disconnect() is called
before DOMContentLoaded, the runtime cancels its pending startup listener so
islands never initialize later against stale DOM.
/revive is a shared page-level singleton, so later named imports reuse the
same runtime instance instead of creating a second one.
3. Add directives to Liquid templates
<product-form client:visible></product-form>
That's a working setup. Any discovered Island whose effective Tag matches
<product-form> is loaded lazily when the directive condition is met. In the
default registeredTag mode, that Tag comes from the file's static
customElements.define("...", ...) call; tagSource: "filename" restores the
v1.x filename-based lookup.
Core Patterns
Configure multiple island directories
shopifyThemeIslands({
directories: ["/frontend/js/islands/", "/frontend/js/components/"],
});
Only add directories when you want every .ts/.js file in that folder to be
treated as an island by convention. Files that import
vite-plugin-shopify-theme-islands/island are discovered independently, even
outside /frontend/js/islands/; listing their parent folder in directories is
redundant and broadens discovery to sibling files that may not be islands.
Override the derived Tag
shopifyThemeIslands({
resolveTag({ filePath, defaultTag }) {
if (filePath.endsWith("/legacy/widget.ts")) return "legacy-widget";
if (filePath.endsWith("/skip-me.ts")) return false;
return defaultTag;
},
});
Use resolveTag() to override Tag derivation for specific files. In the
default registeredTag mode, defaultTag is the Tag read from the file's
static customElements.define("...", ...) call. Returning false excludes
the file from the island map. Returning defaultTag keeps the default.
If two different source files resolve to the same Tag, plugin compilation fails.
Rename, adjust resolveTag, or return false to exclude one file.
Override built-in directive defaults
shopifyThemeIslands({
directives: {
visible: { rootMargin: "0px", threshold: 0.5 },
idle: { timeout: 2000 },
defer: { delay: 5000 },
interaction: { events: ["mouseenter"] },
},
});
Per-directive options are deep-merged — overriding visible.rootMargin preserves visible.threshold at its default of 0.
For config, directives.interaction.events is intentionally narrow and only accepts mouseenter, touchstart, and focusin.
Per-element client:interaction="..." values are checked at runtime against that same set. Unsupported tokens warn and are ignored; if all tokens are unsupported, the runtime warns and falls back to the configured default events.
Per-element client:idle and client:defer values now require strict integer strings. Invalid values warn and fall back to the configured default timeout or delay.
Enable automatic retry with exponential backoff
shopifyThemeIslands({
retry: { retries: 3, delay: 1000 },
});
retries is the number of attempts after the first failure. delay is the base ms — each subsequent retry doubles it (1000ms → 2000ms → 4000ms).
Guard against hung custom directives
shopifyThemeIslands({
directiveTimeout: 5000,
});
When a custom directive never calls load(), the runtime normally waits forever. directiveTimeout turns that into an islands:error event and abandons the activation attempt after the configured number of milliseconds.
Enable console debug output
shopifyThemeIslands({ debug: true });
Logs discovered islands, active directives per element, and load/error events at startup.
Common Mistakes
CRITICAL Virtual module not imported — islands never activate
Wrong:
shopifyThemeIslands({ directories: ["/frontend/js/islands/"] });
Correct:
import "vite-plugin-shopify-theme-islands/revive";
The plugin generates the virtual module but has no effect until it is imported in the browser entry point. Islands are silently never activated.
Source: src/index.ts — VIRTUAL_ID / RESOLVED_ID
HIGH Agent hardcodes default values — unnecessary noise
Wrong:
shopifyThemeIslands({
directories: ["/frontend/js/islands/"],
debug: false,
directives: {
visible: { attribute: "client:visible", rootMargin: "200px", threshold: 0 },
idle: { attribute: "client:idle", timeout: 500 },
media: { attribute: "client:media" },
defer: { attribute: "client:defer", delay: 3000 },
interaction: { attribute: "client:interaction", events: ["mouseenter", "touchstart", "focusin"] },
},
});
Correct:
shopifyThemeIslands();
All options are optional and default to sensible values. Only include options that differ from the defaults.
HIGH Agent adds mixin component folders to directories
Wrong:
shopifyThemeIslands({
directories: ["/frontend/js/islands/", "/frontend/js/components/navigation/"],
});
Correct:
shopifyThemeIslands();
Mixin-marked files are discovered from the project root by their
vite-plugin-shopify-theme-islands/island import. directories is for
non-mixin, convention-scanned files; adding a mixin file's folder does not make
that file more discoverable and can accidentally include unrelated files in the
same folder.
HIGH Agent overwrites existing vite.config.ts instead of appending
Before adding the plugin, read the existing vite.config.ts. Projects commonly
already have vite-plugin-shopify or other plugins — the island plugin must be
added to the existing plugins array, not replace it.
Wrong:
export default defineConfig({
plugins: [shopifyThemeIslands()],
});
Correct:
export default defineConfig({
plugins: [
shopify(),
shopifyThemeIslands(),
],
});
HIGH retry nested inside directives — no retries happen
Wrong:
shopifyThemeIslands({
directives: {
retry: { retries: 2 },
},
});
Correct:
shopifyThemeIslands({
retry: { retries: 2 },
});
directives accepts only visible, idle, media, defer, interaction, and custom. retry at directives.retry is silently ignored.
Source: src/options.ts — ShopifyThemeIslandsOptions
HIGH Wrong key name for retry count
Wrong:
shopifyThemeIslands({ retry: { count: 3 } });
shopifyThemeIslands({ retry: { attempts: 3 } });
Correct:
shopifyThemeIslands({ retry: { retries: 3 } });
Unknown keys are silently ignored. The correct field is retries.
Source: src/contract.ts — RetryConfig
HIGH directiveTimeout nested inside directives — timeout guard never applies
Wrong:
shopifyThemeIslands({
directives: {
directiveTimeout: 5000,
},
});
Correct:
shopifyThemeIslands({
directiveTimeout: 5000,
});
directiveTimeout is a top-level plugin option, not part of the per-directive config object.
Source: src/options.ts — ShopifyThemeIslandsOptions
HIGH Empty or unsupported directives.interaction.events values fail config resolution
Wrong:
shopifyThemeIslands({
directives: {
interaction: { events: [] },
},
});
shopifyThemeIslands({
directives: {
interaction: { events: ["click"] as never[] },
},
});
Correct:
shopifyThemeIslands({
directives: {
interaction: { events: ["mouseenter", "focusin"] },
},
});
The typed config surface only supports the package-owned interaction events mouseenter, touchstart, and focusin. An empty array is rejected because it would otherwise create an interaction gate that never resolves.
Source: src/interaction-events.ts — validateInteractionEvents()
HIGH directories: [] fails plugin validation
Wrong:
shopifyThemeIslands({ directories: [] });
Correct:
shopifyThemeIslands();
shopifyThemeIslands({ directories: ["/frontend/js/islands/"] });
An empty directories array is rejected at config resolution.
Source: src/resolved-config.ts — validateOptions()
HIGH directives.visible.threshold outside 0–1 fails plugin validation
Wrong:
shopifyThemeIslands({
directives: {
visible: { threshold: 1.5 },
},
});
Correct:
shopifyThemeIslands({
directives: {
visible: { threshold: 0.5 },
},
});
threshold must be between 0 and 1 inclusive.
Source: src/resolved-config.ts — validateOptions()