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.

PackageWhat it is
reactThe component runtime
react-domMounts React into the page
swiperCarousels and sliders
lucide-reactIcons

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 placesrc/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' // yes
2import * as Icons from 'lucide-react' // no — pulls in the whole library

Importing 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.

PackageWhat it is
viteThe bundler and dev server
@vitejs/plugin-reactJSX transform and Fast Refresh
tailwindcssThe CSS framework
@tailwindcss/viteTailwind's Vite integration
chokidarFile watching
concurrentlyRuns two dev processes at once
jsdomA 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}';
3
4@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 placevite/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 usedWhy
A state libraryEvery island shares one bundle, so a module-level variable is already shared. createStore is ~40 lines
A data-fetching libraryuseAPI covers dedup, caching and abort in the one shape this theme needs
A routerShopify owns navigation. The theme is islands on server-rendered pages, not an SPA
An i18n libraryLiquid resolves strings with | t before React sees them — see Multi-language
A syntax highlighterOnly the docs site needs one, and it hand-rolls it
TypeScriptJSDoc 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 build
ls -lh dist/assets/main.js

Everything under src/javascript, src/components and src/styles compiles into that one file. If it grows unexpectedly, a namespace import is the usual cause.