Getting started
Packages
Every library the theme uses, and where.
Eleven packages, four of which ship to the browser. Everything else is build tooling and never reaches a storefront.
That number is deliberate. A theme is loaded on every page of a store, so each runtime dependency is paid for by every shopper on every visit — and anything here is something you inherit the maintenance of.
Ships to the browser
These are bundled into assets/main.js.
| Package | What it is |
|---|---|
react | The component runtime |
react-dom | Mounts React into the page |
swiper | Carousels and sliders |
lucide-react | Icons |
react
The whole point of the theme. Sections are React components mounted as islands rather than one application, so a page with no React sections ships no React work at all.
Used in 23 files — every section component, every component under
src/components, and the hooks in src/javascript.
import { useMemo, useState } from 'react'Version 19 specifically, for useSyncExternalStore (which
createStore is built on) and for ref as a
plain prop rather than forwardRef.
react-dom
Imported in exactly one place — src/javascript/islands.jsx, which calls
createRoot for each [data-react-component] on the page.
import { createRoot } from 'react-dom/client'Nothing else should import it. If a component needs a portal or
flushSync, that is worth a conversation first — it usually means the island
boundary is in the wrong place.
swiper
Carousels, in 3 files: the slideshow, the announcement bar, and the shared
Carousel organism.
1import { Swiper, SwiperSlide } from 'swiper/react'2import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'Modules are imported individually rather than as a bundle, so a carousel that does not autoplay does not pay for the autoplay module.
Swiper is the largest runtime dependency. It earns its place by handling touch,
momentum, loop edge cases and keyboard access — but it is the first thing to
question if a page has no carousel on it.
useCarousel wraps the editor-correct wiring
around it.
lucide-react
Icons, in 12 files. Tree-shakeable, inherits currentColor, and every icon
is a plain SVG component.
1import { ShoppingBag } from 'lucide-react' // yes2import * as Icons from 'lucide-react' // no — pulls in the whole libraryImporting the namespace is the one way to get this badly wrong, and it is worth watching for in review — the bundle grows by megabytes and nothing else about the page changes.
Build tooling
None of these reach a storefront.
| Package | What it is |
|---|---|
vite | The bundler and dev server |
@vitejs/plugin-react | JSX transform and Fast Refresh |
tailwindcss | The CSS framework |
@tailwindcss/vite | Tailwind's Vite integration |
chokidar | File watching |
concurrently | Runs two dev processes at once |
jsdom | A DOM for tests |
vite
Compiles src/ into dist/, which is the actual Shopify theme. Configured in
vite.config.js, which loads three plugins:
plugins: [react(), tailwindcss(), shopifyTheme()]The third is ours — vite/shopify-theme.js, which does
the src → dist translation, generates the component and api registries, and
prunes stale output.
@vitejs/plugin-react
The JSX transform, so .jsx files compile without a pragma, plus Fast Refresh
in npm run dev.
tailwindcss and @tailwindcss/vite
Tailwind v4. There is no tailwind.config.js — v4 is configured in CSS, in
src/styles/main.css:
1@import 'tailwindcss';2@source '../**/*.{js,jsx,liquid}';34@theme {5 --color-background: var(--theme-background);6}@source matters: Tailwind scans those files for class names, and a class built
at runtime (`grid-cols-${n}`) is invisible to it. That is why the theme
writes class maps out in full — see Styling.
The @theme block is what connects a merchant's colour scheme to a utility
class, so bg-background follows whatever scheme a section is set to.
chokidar
Loaded lazily, in one place — vite/shopify-theme.js:
1import('chokidar').then(({ default: chokidar }) => {2 watcher = chokidar.watch(WATCH_DIRS, { ignoreInitial: true })3})Rollup only watches modules reachable from the JS entry point, so Liquid, fonts,
media and the API sources need a watcher of their own. Without it, editing a
.liquid file during npm run dev would do nothing.
concurrently
Runs Vite's watcher and shopify theme dev side by side, with prefixed and
coloured output so you can tell which one is talking:
concurrently -n vite,shopify -c cyan,green "npm:watch" "npm:shopify:dev"jsdom
A real DOM for tests. React 19's createRoot needs a genuine element — a
hand-rolled stub gets as far as targetContainer.addEventListener is not a function and stops.
Nothing in src/ imports it. It exists purely so hook tests can render.
What is deliberately absent
Worth knowing what was considered and left out.
| Not used | Why |
|---|---|
| A state library | Every island shares one bundle, so a module-level variable is already shared. createStore is ~40 lines |
| A data-fetching library | useAPI covers dedup, caching and abort in the one shape this theme needs |
| A router | Shopify owns navigation. The theme is islands on server-rendered pages, not an SPA |
| An i18n library | Liquid resolves strings with | t before React sees them — see Multi-language |
| A syntax highlighter | Only the docs site needs one, and it hand-rolls it |
| TypeScript | JSDoc plus jsconfig.json gives editor types without a compile step in the theme build |
The API gateway in src/api has zero dependencies by design — fetch,
crypto.subtle and URL are built into every runtime it targets, which is what
lets a deploy folder be dropped onto any host with nothing to install.
Checking what you actually ship
npm run buildls -lh dist/assets/main.jsEverything under src/javascript, src/components and src/styles compiles
into that one file. If it grows unexpectedly, a namespace import is the usual
cause.