How I Brought Island Architecture to Shopify Themes with Vite and Web Components
How I built a lighter Shopify theme frontend using island architecture, Vite and Web Components, without turning the theme into a framework project.
On this page
- The problem was not “more frontend”
- Why island architecture made sense
- Why Vite and Web Components were the right fit
- The shape of the plugin
- Two ways islands are discovered
- 1. Directory scanning
- 2. Explicit opt-in with the Island mixin
- Why declarative directives matter
- The runtime is where it gets real
- What I was trying to avoid
- Trade-offs
- When I would not use this approach
- What other developers can take from this
- FAQ: island architecture on Shopify liquid themes
- What is island architecture?
- Why use island architecture for a Shopify theme?
- Does Shopify support frameworks like Astro or Next.js?
- Why Web Components instead of React in a theme?
- Will island architecture break my Shopify theme’s SEO?
- Closing thought
Shopify themes have an awkward frontend problem.
They are not full single-page apps, but they are not truly static either. Real themes end up carrying product forms, drawers, recommendations, filters, tabs, accordions, media galleries, promo blocks, and a growing pile of third-party scripts. Over time, the JavaScript footprint grows, the loading strategy gets inconsistent, and the theme starts feeling heavier than it should.
That was the problem I wanted to solve with vite-plugin-shopify-theme-islands (opens in new tab).
Not “how do I build a frontend app in Shopify?”
More: how do I load less JavaScript, keep the runtime lean, and still make interactive parts of a theme feel modern?
This was not just a technical neatness problem. Heavy frontend patterns slow delivery, increase regression risk, and make simple theme changes feel expensive. I wanted an architecture a real team could maintain without turning every iteration into a frontend project.
The answer I ended up with was island architecture, built on Vite and Web Components.
The problem was not “more frontend”#
There is a temptation, especially in frontend circles, to treat every interaction problem as a reason to add more framework.
Sometimes that is the right move. Often it is not.
In Shopify themes, a lot of the page is still best treated as Liquid-rendered, server-first markup. Shopify’s theme architecture is built around templates, sections, blocks, snippets, and assets, so the challenge is usually not “how do I hydrate everything?” It is “which parts actually need JavaScript, and when?”
A cart drawer probably should not load the moment the page does if the customer never opens it. Product recommendations below the fold do not need to ship immediately. A mobile-only navigation component should not cost desktop users anything if they never need it.
That is where the usual approaches start to feel unsatisfying:
- a heavier frontend framework can be overkill for many themes
- one-off lazy loading rules become hard to reason about
- component-by-component script loading gets messy fast
- different developers solve the same loading problem in different ways
You end up with the worst kind of frontend architecture: not simple enough to stay boring, not structured enough to stay clear. That usually means slower releases, more QA overhead, and more time spent debugging loading behaviour instead of shipping value.
Why island architecture made sense#
Island architecture is useful when a page is mostly static, but specific interactive parts need to hydrate independently.
That fits Shopify themes pretty well.
Astro (opens in new tab) was a real influence here.
It helped popularise a practical, mainstream version of the pattern: keep the page mostly server-rendered HTML, then hydrate only the components you explicitly mark as interactive.
That was the part I wanted to bring across, not Astro itself. The idea was not “put Astro inside Shopify.” It was “borrow the selective hydration model and make it feel natural in Liquid themes.”
The goal is simple:
- render the page normally on the server
- mark the interactive parts clearly
- load each island only when it actually needs to become interactive
That gives you a better default than shipping all component JavaScript up front.
More importantly, it lets performance decisions live closer to the actual component. Instead of one giant bundle and one global loading model, each island can say what it needs.
For example:
<product-recommendations client:visible></product-recommendations>
<recently-viewed client:idle></recently-viewed>
<cart-flyout client:interaction></cart-flyout>That is the kind of API I wanted: declarative, readable, and close to the Liquid markup developers are already working with in sections, snippets, and templates.
I liked the idea that loading behaviour should be visible in markup instead of buried in component internals or one global bundle.
Why Vite and Web Components were the right fit#
I was not trying to recreate an app framework inside Shopify.
The problem was narrower than that. I wanted progressive enhancement, low runtime overhead, and something a real theme codebase could adopt without a rewrite.
That is why the package ended up built around Vite and Web Components. They felt like the right tools for the job, not the loudest tools in the room.
Vite gave me the build-time surface I needed:
- discover island modules
- generate a runtime entry point
- support convention-based discovery
- still allow explicit opt-in where needed
That kept the authoring model straightforward:
import { defineConfig } from "vite";
import shopifyThemeIslands from "vite-plugin-shopify-theme-islands";
export default defineConfig({
plugins: [shopifyThemeIslands()],
});import "vite-plugin-shopify-theme-islands/revive";The separation mattered. The plugin handles discovery and wiring at build time. The browser runtime handles activation.
Web Components were the other half of the answer. A cart drawer, product form, media gallery, or tab switcher already wants to behave like a self-contained custom element.
Using custom elements meant I could stay close to browser primitives:
- a clear element boundary
- predictable upgrade behaviour
- no need to mount a whole framework runtime just to enhance one small part of the page
That does not make Web Components the answer to everything. It just means they matched this problem better than importing more framework machinery than the theme actually needed.
That is the broader pattern I keep coming back to in frontend work: the best tool is usually the one that solves the actual problem cleanly, not the one that makes the architecture feel most impressive on paper.
AI-assisted workflows shaped the build itself in ways worth being honest about. AI was useful for pattern-matching across legacy theme code (finding every place a custom event was dispatched, every implicit hydration assumption) and for drafting the boilerplate around directive parsing and runtime registration. The architectural calls — convention-based discovery, AND semantics for directives, where the build/runtime split lives — were human decisions, made because they had to be defensible against real Shopify theme constraints. AI is good at surfacing options. It is not good at choosing between them when the trade-offs only exist in the head of someone who has shipped this kind of code before.
The shape of the plugin#
At a high level, the package has two jobs.
At build time, it discovers island modules and generates a revive module. In the browser, the runtime walks the DOM, matches custom elements to those islands, and decides when each one should load.
That makes the system feel simple from the outside:
- define an island
- use it in Liquid templates, sections, or snippets
- add a directive if it should load conditionally
But under the hood, there are a few practical decisions that matter.
Two ways islands are discovered#
One of the most useful decisions in the package is that islands can be discovered in two different ways.
1. Directory scanning#
If a file lives in the configured islands directory, it becomes an island automatically. The filename maps to the custom element tag.
That is good when you want a clear convention and one place to audit interactive components.
2. Explicit opt-in with the Island mixin#
If a component already lives somewhere else, you can explicitly mark it as an island without moving the file.
import Island from "vite-plugin-shopify-theme-islands/island";
class SiteFooter extends Island(HTMLElement) {
connectedCallback() {
// ...
}
}That sounds like a small thing, but it matters in real projects.
Pure convention sounds clean until it forces awkward file moves or makes incremental adoption harder than it needs to be. Explicit opt-in sounds clean until everything becomes manual and inconsistent. Supporting both gave the package a better adoption story than forcing one “correct” structure.
Why declarative directives matter#
The other important design choice was making loading behaviour declarative.
Instead of hiding lazy-loading logic in component internals, the loading condition can live in the markup:
<product-recommendations client:visible></product-recommendations>
<mobile-menu client:media="(max-width: 768px)"></mobile-menu>
<cart-flyout client:interaction></cart-flyout>That buys a few things:
- the loading rule is visible where the component is used
- different components can follow different strategies
- the runtime has a clear, predictable contract
It also keeps the theme code easier to reason about. A developer looking at the template can see not just what component is present, but roughly when its JavaScript is meant to load.
That is a better default than burying everything behind one big bundle and hoping the runtime figures itself out.
The directive system gets its own follow-up article, because it deserves more than a quick mention here. But for the broader architecture story, the key point is that directives made the loading model explicit.
The runtime is where it gets real#
The hardest part was not “load a component lazily”.
The hardest part was making the runtime behave sensibly once real theme DOM got involved.
A Shopify theme is not a perfect demo environment. You get nested components, dynamically added content, components that should not wake up too early, and interaction patterns that do not always happen in a neat order.
That means the runtime has to handle things like:
- walking the DOM for matching custom elements
- respecting directive order
- avoiding premature activation of child islands inside queued parents
- handling elements added after the initial page load
- retrying failed loads sensibly
That is the kind of complexity that is easy to underestimate when the architecture still looks clean on a whiteboard.
The interesting engineering work was less about inventing a flashy loading system and more about making the runtime predictable when the page stopped being idealised. That is usually the more useful kind of frontend work anyway.
What I was trying to avoid#
A lot of the package design came from restraint.
I was trying to avoid:
- a framework-shaped solution to a narrower problem
- a runtime that felt magical or hard to debug
- feature creep that made the plugin harder to adopt
- an API that looked elegant in docs but awkward in theme code
- forcing projects into the wrong tool just because it looked more modern
That is also why I like this package as a case study. It is not really about Shopify alone. It is about a more general frontend principle: solve the problem you actually have, and stop before the architecture starts solving imaginary ones as well.
Trade-offs#
This is not the right answer for every frontend stack.
If you are building a genuinely app-heavy interface, you may want a different architecture. Web Components also come with their own ergonomics trade-offs, and lazy-loading interactive code does not remove the need for good component boundaries or sensible state management.
Even here, the runtime is not “free”. Coordinating discovery, directives, nested elements, and DOM updates still adds complexity. It is just complexity I would rather have than a heavier client stack that solves a broader problem than the one the theme actually has.
That trade-off felt worth it.
When I would not use this approach#
- when the product is genuinely app-heavy and needs richer client-side state as a first-class concern
- when the team does not want to work with Web Components at all
- when the main bottleneck is not frontend loading but backend constraints or product scope churn
This approach works best when the problem is “too much JavaScript for a mostly server-rendered theme”, not “we need an SPA”.
What other developers can take from this#
The biggest lesson for me was not really “use islands”.
It was:
- choose the lightest architecture that fits the problem
- keep progressive enhancement in view
- make adoption flexible
- make runtime behaviour explicit
- design APIs around how people really build and maintain sites
That last part matters more than most people admit. A technically clever system that is awkward to adopt usually loses to a simpler one that fits the codebase people already have.
That is what I wanted from this plugin: not the most ambitious frontend architecture, just one that a real Shopify project could live with.
FAQ: island architecture on Shopify liquid themes#
What is island architecture?#
A pattern where most of the page is server-rendered HTML and only specific interactive regions (“islands”) hydrate as small, independent client-side components. Non-island regions do not need each island’s JavaScript until that island activates—but the storefront still ships whatever else you load: theme scripts, third-party tags, the revive entry, and eager imports from theme code. This approach only gates JS for custom elements you register as islands (for example behind <your-element client:*>), not every script on the page. It originated in static-site frameworks like Astro and adapts well to server-rendered themes like Shopify’s Liquid.
Why use island architecture for a Shopify theme?#
Shopify themes are already server-rendered. Most theme JavaScript is wholesale-loaded for a few interactive components — cart drawers, search, swatches, modals. Island architecture lets you ship interactivity per-component, lazily, and only when needed, which usually cuts main-thread work during initial load, directly helps INP and TBT, and can improve LCP indirectly when island JavaScript was competing for main-thread time or bandwidth with the LCP resource path.
Does Shopify support frameworks like Astro or Next.js?#
Not as a theme runtime. Shopify themes are Liquid-rendered server-side; the framework story sits on top of that, in the form of how you author and load any client-side code. Hydrogen is a separate Shopify offering for fully headless storefronts and is a different decision altogether.
Why Web Components instead of React in a theme?#
Web Components give you encapsulation, native browser support, and zero runtime framework cost — which matters when the theme is the host environment. For islands that need to coexist with server-rendered Liquid and avoid framework lock-in inside a theme, Web Components are usually the simpler, lighter choice.
Will island architecture break my Shopify theme’s SEO?#
No, if implemented correctly. Server-rendered HTML is the default; islands hydrate progressively on top. Crawlers see the same content they always did. The performance gains usually help rankings rather than hurt them.
Closing thought#
Bringing island architecture to Shopify themes was less about following a trend and more about finding a sensible boundary.
I wanted a way to keep server-rendered themes mostly simple, load interaction when it was actually needed, and avoid turning the frontend into something heavier than the problem deserved. Vite and Web Components ended up being a practical way to do that.
The follow-up articles get into the directive system and the package-design side. But the broad takeaway is simpler than that: if you can solve the problem with a leaner architecture, you probably should.
XIII Studios (opens in new tab) builds high-performance websites and software without the usual agency overhead. If your Shopify frontend feels heavier than it should and you want a straight answer on what is actually worth building, get in touch (opens in new tab) - we’re happy to talk it through.

