| name | directives |
| description | Built-in client directives: client:visible (IntersectionObserver, rootMargin), client:media (matchMedia query), client:idle (requestIdleCallback), client:defer (setTimeout delay), client:interaction (mouseenter/touchstart/focusin). Directives resolve sequentially — visible → media → idle → defer → interaction → custom. Per-element value overrides. Empty client:media warning. `client:idle` and `client:defer` require strict integer strings; invalid values warn and fall back. `client:interaction` validates per-element tokens at runtime: whitespace-only values warn and fall back; mixed supported/unsupported values warn and ignore the unsupported tokens; fully unsupported values warn and fall back to default events. Global `directives.interaction.events` config is intentionally narrowed to the curated set `mouseenter`, `touchstart`, and `focusin`. Implementation: src/directive-spine.ts (gates), src/directive-waiters.ts (built-in waits), src/activation-session.ts (sequencing, custom latch, loader).
|
| metadata | {"type":"core","library":"vite-plugin-shopify-theme-islands","library_version":"2.0.1"} |
| sources | ["Rees1993/vite-plugin-shopify-theme-islands:src/directive-spine.ts","Rees1993/vite-plugin-shopify-theme-islands:src/directive-waiters.ts","Rees1993/vite-plugin-shopify-theme-islands:src/activation-session.ts","Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts","Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts","Rees1993/vite-plugin-shopify-theme-islands:src/resolved-config.ts","Rees1993/vite-plugin-shopify-theme-islands:src/interaction-events.ts"] |
Setup
Add one or more directives as HTML attributes on any custom element:
<product-form client:visible></product-form>
<mobile-nav client:media="(max-width: 768px)"></mobile-nav>
<site-footer client:idle></site-footer>
<chat-widget client:defer="5000"></chat-widget>
<cart-flyout client:interaction></cart-flyout>
No JS changes needed — the runtime reads these attributes during DOM walk.
Core Patterns
Combining directives — sequential resolution order
Directives resolve in a fixed order: visible → media → idle → defer → interaction → custom. Each condition is only evaluated after the previous one has passed.
<mega-menu client:visible client:interaction></mega-menu>
<product-recommendations client:visible client:media="(min-width: 768px)"></product-recommendations>
Combined directives are AND-latched. The island loads only after every condition resolves. There is no OR mode.
Per-element value overrides
<hero-banner client:visible="0px"></hero-banner>
<analytics-widget client:idle="2000"></analytics-widget>
<chat-widget client:defer="8000"></chat-widget>
<cart-flyout client:interaction="mouseenter"></cart-flyout>
The attribute value overrides the globally configured default for that element. Other elements are unaffected.
In config, directives.interaction.events is stricter and only accepts the curated package-owned list: mouseenter, touchstart, and focusin.
At runtime, per-element client:interaction values use that same curated set. Unsupported tokens are ignored with a warning; if no supported tokens remain, the runtime warns and falls back to the default interaction events.
For client:idle and client:defer, values like "20ms" are now invalid and fall back to the configured default timeout or delay.
client:defer without a value uses the global default
<chat-widget client:defer></chat-widget>
<chat-widget client:defer="0"></chat-widget>
An empty client:defer attribute is NOT zero — it falls back to the configured defer.delay (default 3000ms).
client:interaction with no value uses the default events
<cart-flyout client:interaction></cart-flyout>
<cart-flyout client:interaction="mouseenter"></cart-flyout>
An empty client:interaction attribute uses the configured default events with no warning. A whitespace-only value such as client:interaction=" " emits a warning and still falls back to the default events.
Source: src/directive-spine.ts and src/activation-session.ts — interaction token parsing and fallback warning
Mixed supported and unsupported interaction tokens
<cart-flyout client:interaction="mouseenter click"></cart-flyout>
<cart-flyout client:interaction="click submit"></cart-flyout>
Per-element values are no longer treated as an unconstrained event surface. The runtime filters them against the curated package-owned set.
Changing built-in directive defaults globally
shopifyThemeIslands({
directives: {
visible: { rootMargin: "0px" },
defer: { delay: 5000 },
interaction: { events: ["mouseenter"] },
},
});
Removed elements abort waiting directives silently
<hero-banner client:visible></hero-banner> <cart-flyout client:interaction></cart-flyout>
If either element is removed from the DOM before its directive resolves, the runtime cancels that activation attempt and does not dispatch islands:error. This is expected teardown behavior, not a load failure.
Common Mistakes
HIGH client:media="" skips the media check entirely
Wrong:
<mobile-nav client:media=""></mobile-nav>
Correct:
<mobile-nav client:media="(max-width: 768px)"></mobile-nav>
An empty client:media value emits a console warning and skips the media check — the island loads immediately. Provide a valid media query string.
Source: src/directive-spine.ts and src/activation-session.ts — empty media gate handling
MEDIUM client:idle and client:defer do not accept suffix junk
Wrong:
<analytics-widget client:idle="2000ms"></analytics-widget>
<chat-widget client:defer="3s"></chat-widget>
Correct:
<analytics-widget client:idle="2000"></analytics-widget>
<chat-widget client:defer="3000"></chat-widget>
These attributes now require strict integer strings. Invalid values warn and fall back to the configured default timeout or delay.
MEDIUM Whitespace-only client:interaction value warns and falls back
Wrong:
<cart-flyout client:interaction=" "></cart-flyout>
Correct:
<cart-flyout client:interaction></cart-flyout>
<cart-flyout client:interaction="mouseenter focusin"></cart-flyout>
Whitespace-only values are not treated the same as an empty attribute. The runtime warns and falls back to the configured default events.
Source: src/directive-spine.ts — interaction gate parsing and whitespace fallback
MEDIUM Unsupported per-element interaction tokens are warned and ignored
Wrong:
<cart-flyout client:interaction="mouseenter click"></cart-flyout>
Correct:
<cart-flyout client:interaction="mouseenter focusin"></cart-flyout>
The runtime no longer attaches arbitrary listeners for unsupported per-element tokens. Supported tokens still work; unsupported ones are ignored with a warning. If no supported tokens remain, the runtime falls back to the configured default events.
Source: src/directive-spine.ts and src/activation-session.ts — supported/unsupported interaction token handling
HIGH Multiple directives are AND, not OR
Wrong assumption:
<product-recs client:visible client:media="(min-width: 768px)"></product-recs>
Correct understanding:
<product-recs client:visible client:media="(min-width: 768px)"></product-recs>
The runtime awaits each directive sequentially. There is no way to express OR semantics with built-in directives — use a custom directive for that.
Source: src/activation-session.ts — runBuiltInDirectives() runs built-ins before custom directives
MEDIUM client:defer without value ≠ immediate load
Wrong:
<chat-widget client:defer></chat-widget>
Correct:
<chat-widget client:defer="0"></chat-widget>
client:defer with no value uses the global defer.delay default (3000ms). parseInt("", 10) produces NaN, which the runtime replaces with the configured default.
Source: src/activation-session.ts — defer parsing and fallback to directives.defer.delay
MEDIUM Per-element visible value replaces rootMargin, not adds to it
Wrong:
<hero-banner client:visible="100px"></hero-banner>
Correct:
<hero-banner client:visible="100px"></hero-banner>
The attribute value is passed directly to IntersectionObserver as rootMargin, fully replacing the global default.
Source: src/activation-session.ts — visible attribute value replaces directives.visible.rootMargin
HIGH Directive attribute typo — island loads without condition
Wrong:
<product-form client:visibled></product-form> <product-form client:Visible></product-form>
Correct:
<product-form client:visible></product-form>
Directive attributes are case-sensitive. An unrecognised attribute is silently ignored — the island loads immediately as if no directive were set. No warning is emitted. Check for typos if an island activates earlier than expected.
Source: src/directive-spine.ts — built-ins read exact configured attribute names from the spine
HIGH Unsupported interaction events in config fail plugin setup
Wrong:
shopifyThemeIslands({
directives: {
interaction: { events: ["click"] as never[] },
},
});
Correct:
shopifyThemeIslands({
directives: {
interaction: { events: ["mouseenter", "focusin"] },
},
});
The package now owns a curated interaction-event vocabulary for config. Supported values are mouseenter, touchstart, and focusin; unsupported names and empty arrays are rejected during config resolution.
Source: src/interaction-events.ts — validateInteractionEvents()
HIGH Agent uses default attribute name when developer has configured a custom one
Wrong:
<product-form client:visible></product-form>
Correct:
<product-form data:visible></product-form>
When directives.visible.attribute (or any directive's attribute option) is overridden in vite.config.ts, all Liquid templates must use the configured name. The default client:* names no longer apply. Always read vite.config.ts to check for overridden attribute names before writing directives in Liquid.
Source: src/options.ts — DirectivesConfig attribute field per directive; src/directive-spine.ts reads configured attribute names at runtime