Skip to main content

Theme Overview

A theme controls the look and feel of your EverShop storefront. It allows you to override React components, add new page components, customize styles, and serve your own static assets — all without modifying the core codebase.

warning

Themes only affect storefront (frontStore) pages. The admin panel cannot be customized through themes. To modify admin pages, use an extension instead.

Out-of-the-box, EverShop renders the storefront using components from its core modules. A theme provides an overlay that can override any of these components or add new ones. When EverShop builds a page, it checks the theme first — if a matching component exists in the theme, it takes precedence over the core version.

We recommend creating a new theme rather than modifying core files directly, as core changes are overwritten during upgrades.

Where Are Themes Located?​

The Default Storefront (No Theme)​

When no theme is configured, EverShop renders the storefront using components from its core modules. Each core module (catalog, checkout, customer, etc.) has a pages/frontStore/ folder with React components that define the default UI. You can think of this as the "built-in theme."

info

Learn more about how module pages work in the View System documentation.

Custom Themes​

Custom themes are located in the themes/ folder at the root of your project:

your-project/
├── themes/
│ ├── my-theme/
│ └── another-theme/
├── extensions/
├── config/
└── package.json

Each theme must be stored in a separate directory:

*/themes/
├── <theme1>
├── <theme2>
├── <theme3>
├── ...

Creating a Theme​

The fastest way to create a new theme is with the CLI:

npx evershop theme:create

The command is interactive — it prompts for the theme name and takes no arguments or flags. The name must be alphanumeric with dashes or underscores only.

warning

There is no --name flag. theme:create never reads argv, so npx evershop theme:create --name my-theme still prompts you for a name and ignores the flag entirely.

This generates a scaffold in themes/<name>/ with a package.json, a tsconfig.json, a tsconfig.build.json, a scripts/copy-assets.mjs build helper, and a starter homepage component at src/pages/homepage/<Name>.tsx. <Name> is the theme name in capitalized words: sweet-haven becomes SweetHaven.tsx. The build files are explained under The package.json File.

After creating the theme, add themes/* to your root package.json workspaces (if not already there) and install dependencies:

npm install

Then activate the theme and start developing:

npx evershop theme:active
npm run dev

For a detailed guide on customizing components and styles, see the Templating and Styling docs.

Theme Structure​

Theme Name​

A theme's folder name is used as the theme name. Make sure you don't include any whitespace or special characters in the directory name of your theme.

The structure of an EverShop theme directory typically looks like the following:

/themes/
<themeName>/
├── public # Public assets for storing images, fonts, etc.
├── dist # Compiled code of the theme.
├── src # Source code of the theme in TypeScript.
│ ├── components # React components. Contains shared components that can be used in multiple pages.
│ └── pages # Every sub-folder represents a page.
│ ├── all # Components located in this folder will be used in all pages.
│ │ ├── All.tsx # Master level components. This component will be included in the layout of all pages.
│ ├── categoryView
│ │ └── FreeShippingBanner.tsx # Page-specific components.
│ ├── checkout
│ │ └── CheckoutOnly.tsx # Page-specific components.
│ └── homepage
│ └── HomepageOnly.tsx # Page-specific components.
├── theme.json # Theme content manifest (optional). Widgets, placements, landing pages, metafield definitions.
├── layouts.json # Layout overrides (optional). Move core or extension page components without forking them.
├── scripts
│ └── copy-assets.mjs # Build helper. Copies stylesheets and other non-TypeScript files from src to dist.
├── package.json # Theme package file.
├── tsconfig.json # TypeScript configuration for your editor.
└── tsconfig.build.json # TypeScript configuration used by `npm run build`.

The theme.json File​

theme.json is the theme's content manifest. Where src/ ships the code, theme.json ships the data a theme needs in the database to look the way it is meant to look: the widget instances it defines, where those widgets are placed, the landing pages it ships, and the metafield definitions its components read.

themes/yourtheme/theme.json
{
"theme_name": "yourtheme",
"version": "1.0.0",
"widgets": [
{
"uuid": "3f39b388-4025-4237-9df4-344a8b79ad26",
"type": "coupon_block",
"name": "Coupon block",
"settings": {
"heading": "Take 20% off your order",
"code": "SAVE20"
}
}
],
"placements": [
{
"uuid": "5cf1e84f-bbfe-41c1-b2f1-4c357848d953",
"widget_instance_uuid": "3f39b388-4025-4237-9df4-344a8b79ad26",
"route": "homepage",
"area": "content",
"sort_order": 201.25
}
],
"metafieldDefinitions": []
}
FieldRequiredDescription
theme_nameNoFree-form display name. Not validated and not used for matching — the theme's id is always its folder name.
versionYesValid SemVer. Content is installed and upgraded by version; a downgrade is refused. Bump it whenever you change widgets or placements — an unchanged version means the installer treats the content as already applied and skips it.
widgetsYesArray of widget instances: uuid (v4), type, name, settings.
placementsYesArray of placements binding a widget instance to an Area on a route: uuid (v4), widget_instance_uuid, route, area, sort_order. Theme manifests carry route-level placements only — entity_urn must be absent.
metafieldDefinitionsNoMetafield definitions the theme's components read. Provisioned separately from the widget content — no version bump needed for changes, and re-ensured on every server boot.

A theme without a theme.json is a presentation-only theme: activation logs that there is no content to install and proceeds.

info

theme.json is installed by theme:active (see Activating a Theme below). The metafieldDefinitions array is covered in depth in Using Metafields in a Theme.

The package.json File​

The package.json file is used to define the theme's metadata, dependencies, and scripts. It should be located in the root directory of your theme.

Here's an example of a package.json file for a theme:

themes/yourtheme/package.json
{
"name": "yourtheme",
"version": "1.0.0",
"description": "A custom theme for EverShop",
"type": "module",
"private": true,
"scripts": {
"build": "tsc -p tsconfig.build.json --noCheck && node ./scripts/copy-assets.mjs"
}
}

The build script compiles the source files from src/ to dist/ in two steps. tsc compiles the TypeScript with tsconfig.build.json, and scripts/copy-assets.mjs then copies every other file under src/, such as stylesheets, into dist/. --noCheck skips type-checking, which your editor already does through tsconfig.json. You don't need to install EverShop, PostCSS, or Webpack as theme dependencies — the main EverShop project handles the build pipeline.

theme:create writes tsconfig.build.json and scripts/copy-assets.mjs for you and sets this build script. If your theme was scaffolded by an earlier release and its build script is just "tsc", or an swc command, add the two files described in The tsconfig.build.json File and The scripts/copy-assets.mjs File, and replace the script.

Do not build with bare tsc, or with swc and no config

Two builds look right and ship a broken theme:

  • Bare tsc emits .js files only. It does not copy .css or .scss files into dist/, so a theme with stylesheets compiles cleanly and then renders with no styles. This only shows in production (npm run start), never in npm run dev, because the dev server compiles the theme itself.
  • swc with no .swcrc uses an old default target (ES5) and rewrites export const layout to export var layout. EverShop reads each page component's layout from the compiled text with a pattern that requires const, so every page component in the theme silently disappears.
info

Run it from the theme directory (or npm run build --workspace=themes/yourtheme). It is separate from the project's own npm run build, which bundles the storefront — the theme must be compiled to dist/ first.

warning

Since EverShop is built on ESM modules, ensure that your theme’s package.json file has the type field set to "module".

Add the themes directory to the workspaces section of your root package.json. This enables each theme to function as an independent package with its own dependencies.

package.json
{
"workspaces": ["themes/*"]
}

The tsconfig.json File​

The tsconfig.json file is used to configure the TypeScript compiler options for your theme. It should be located in the root directory of your theme. Here's an example of a tsconfig.json file for a theme:

themes/yourtheme/tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"target": "ES2018",
"lib": ["dom", "dom.iterable", "esnext"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"allowJs": true,
"checkJs": false,
"jsx": "react",
"outDir": "./dist",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"allowArbitraryExtensions": true,
"strictNullChecks": true,
"baseUrl": ".",
"rootDir": "src",
"paths": {
"@components/*": [
"./src/components/*",
"../../node_modules/@evershop/evershop/src/components/*"
]
}
},
"include": ["src"]
}
src, not dist, in tsconfig.json

The @components/* path here exists only so your editor and tsc can resolve the alias — it points at the core TypeScript sources (.../@evershop/evershop/src/components/*), which is what theme:create emits and what carries the type information.

That is a different mapping from the one the runtime uses. At build time, webpack resolves @components against dist/components/ in theme → extensions → core order. See Templating.

npm run build does not use this mapping either. See The tsconfig.build.json File.

The tsconfig.build.json File​

tsconfig.build.json is the TypeScript configuration that npm run build uses. It sits next to tsconfig.json in the root directory of your theme:

themes/yourtheme/tsconfig.build.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {},
"declaration": false,
"sourceMap": false
},
"include": ["src"]
}

It extends the editor configuration and empties paths. The @components/* mapping in tsconfig.json points at EverShop's TypeScript sources. If the build kept it, tsc would pull those sources into the program and write compiled files next to them instead of into your dist/.

The scripts/copy-assets.mjs File​

tsc only writes compiled JavaScript files. It leaves .css, .scss, image files and every other asset behind. scripts/copy-assets.mjs copies those from src/ into dist/, keeping the folder layout:

themes/yourtheme/scripts/copy-assets.mjs
import { cpSync, existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const src = resolve(root, 'src');
const dist = resolve(root, 'dist');
if (!existsSync(src)) process.exit(0);
let copied = 0;
cpSync(src, dist, {
recursive: true,
filter: (from) => {
if (/\.tsx?$/.test(from)) return false;
if (!/\.[a-z0-9]+$/i.test(from)) return true; // directory
copied += 1;
return true;
}
});
console.log(`copy-assets: ${copied} non-TypeScript file(s) copied into dist/`);

The GlobalCss.tsx and TailwindCss.tsx components of a theme import their stylesheets by relative path, so a dist/ without these files builds a storefront with no styles at all.

The public Folder​

The public folder stores public assets such as images, fonts, CSS, etc. You can use these assets in your theme by using the public folder as the base path.

You can access a file like public/images/logo.png using the following code:

<img src="/images/logo.png" alt="Logo" />

Or with the StaticImage component:

import { StaticImage } from "@components/common/StaticImage";

function Logo() {
return (
<StaticImage
subPath="images/logo.png"
width={200}
height={60}
alt="Company Logo"
/>
);
}

The pages Folder​

The pages folder is used to add new components or overrides the core components of existing pages. For example, if you want to add a new component to the homepage, you can create a new file in the pages/homepage folder.

In the example structure above, we have a file named HomepageOnly.tsx in the pages/homepage folder. This file will be used to add a new component that appears only on the homepage.

info

Check out the Templating system document to learn how to add a component to a specific page and specify its position.

The components Folder​

The components folder stores shared components that can be used across multiple pages. For example, if you want to create a component that will be used on both the homepage and category pages, you should place it in the components/common folder.

The layouts.json File​

Every page component declares its own position:

export const layout = {
areaId: 'headerMiddleCenter',
sortOrder: 10
};

To move a component that the theme does not own, you do not need to copy its file. layouts.json at the theme root overrides the position of any storefront page component, keyed the same way page components are resolved, <routeFolder>/<Name>:

themes/yourtheme/layouts.json
{
"all/SearchBox": { "areaId": "headerBottom", "sortOrder": 10 },
"all/Breadcrumb": { "sortOrder": 1 },
"productView/Description": { "areaId": "productPageMiddleRight", "sortOrder": 40 }
}

The last entry moves the product description from its own block under the gallery into the summary column, after the product name (sortOrder 10) and the buy box (sortOrder 30).

Blocks you can move. The product and category pages are built as a shell plus blocks. The shell (ProductView, CategoryView) owns the data, the context and the slots (Areas); each block is a page component that registers into a slot. These are the block keys and their default slots:

BlockDefault areaSort orderRenders
productView/ProductMediaproductPageMiddleLeft0Image gallery
productView/ProductNameproductPageMiddleRight10Product name (with its productNameBefore / productNameAfter areas)
productView/ProductFormproductPageMiddleRight30Buy box: variant selector, quantity, add to cart
productView/ProductPriceproductSinglePageForm5Price row, inside the buy box by default
productView/ProductAttributesproductSinglePageForm7Attribute list, inside the buy box by default
productView/ProductDescriptionproductSingleDescription10Description (with its productDescriptionBefore / productDescriptionAfter areas)
categoryView/CategoryInfocategoryInfo10Category name, description and image
categoryView/CategoryFiltercategoryLeftColumn10Filter navigation
categoryView/CategorySortingcategoryRightColumn10Sort control with the product count
categoryView/CategoryProductscategoryRightColumn20Product list
categoryView/CategoryPaginationcategoryRightColumn30Pagination
cart/CartTitleshoppingCartHeader10Title and item count
cart/CartItemsshoppingCartItems10Item list
cart/CartSummaryshoppingCartSummary10Order summary card with the checkout button (and the shoppingCartBeforeSummary, shoppingCartBeforeCheckoutButton, shoppingCartAfterSummary areas)
checkout/CheckoutContactcheckoutSteps10Contact information step
checkout/CheckoutShipmentcheckoutSteps20Shipping step
checkout/CheckoutPaymentcheckoutSteps30Payment step
checkout/CheckoutShippingNotecheckoutSteps40Order note inside the form flow, shown on small screens only
checkout/CheckoutPlaceOrdercheckoutSteps50Place-order button
checkout/CheckoutSummarycheckoutSummary10Desktop summary rail: order note and order summary card
account+orderList/AccountHeaderaccountPageHeader10Account header with the logout button (dashboard and order list)
account+orderList/AccountNavaccountPageHeader20Dashboard / Orders tabs (dashboard and order list)
account/AccountRecentOrdersaccountPageContent10Recent orders section
account/AccountInfoaccountPageContent20Account information section
account/AccountAddressBookaccountPageContent30Address book section (with the accountPageAddressBook area)
orderList/CustomerOrdersaccountPageContent10Order list
blogHome/BlogHomeHeaderblogListHeader10Blog title
blogCategoryView/BlogCategoryHeaderblogListHeader10Category name and description
blogTagView/BlogTagHeaderblogListHeader10Tag name
blogHome+blogCategoryView+blogTagView/BlogPostsblogListContent10Post grid, or the empty message (blog home, category and tag pages)
blogHome+blogCategoryView+blogTagView/BlogListPaginationblogListContent20Pagination (blog home, category and tag pages)
blogPostView/BlogPostHeaderblogPostArticle10Back link, category, title and meta line
blogPostView/BlogPostThumbnailblogPostArticle20Thumbnail
blogPostView/BlogPostBodyblogPostArticle30Post content
blogPostView/BlogPostTagsblogPostArticle40Tag chips
blogPostView/BlogPostReactionsblogPostArticle50Reaction bar
blogPostView/BlogPostShareblogPostArticle60Share buttons
blogPostView/BlogPostRelatedblogPostBottom10Related posts
blogPostView/BlogPostCommentsblogPostBottom20Comment section
catalogSearch/SearchInfosearchPageContent10Search heading with the result count
catalogSearch/SearchProductssearchPageContent20Result grid

A block reads its page's context (the product, the category, the cart, the checkout, the customer), so it must stay inside that page's areas. A block placed outside them throws at render time, because the context it reads is not there. That is intended: layouts.json is a developer file, and the error is the feedback. Two more constraints follow from the same rule: the checkout step blocks register fields on the checkout form, so they must stay inside the form's areas (checkoutFormBefore, checkoutSteps, checkoutForm, checkoutFormAfter), while the place-order button only reads the checkout context and can go anywhere in the page, the summary rail included. The variant selector and the add-to-cart controls are not blocks: they belong to the buy box form and stay inside it.

Blocks in a routeA+routeB folder are shared by those routes, so one layouts.json entry moves them on every page that renders the slot, and one override file re-skins them everywhere. The blog listing pages also expose empty blogListTop and blogListBottom slots, and the post page a blogPostTop slot, for extensions and widgets. Blocks are route-scoped: an entry can change a block's area and order, never the page it belongs to.

If your theme overrides one of these shells from an earlier version (ProductView.tsx, CategoryView.tsx, ProductSingleForm.tsx, ShoppingCart.tsx, Checkout.tsx, MyAccount.tsx, OrderList.tsx, BlogHome.tsx, BlogCategoryView.tsx, BlogTagView.tsx, BlogPostView.tsx, SearchPage.tsx), remove the inline pieces that core now ships as blocks from your copy, or your page renders them twice.

How it behaves:

RuleMeaning
Placement onlyThe map changes where a component renders, never which file renders. Component files are still resolved core → extensions → theme, and the map moves whichever file won that key. Props, queries and ids are untouched.
Partial valuesGive areaId, sortOrder, or both. Anything omitted keeps the value from the component file.
Highest priorityAn entry wins over the file's own layout, including files inside the theme itself. Placement for the whole theme is answered in this one file.
Theme only, storefront onlyOnly the active theme's file is read, and admin routes are never affected.
LooseA missing or malformed file, a key that matches no component, or a value of the wrong shape is ignored silently.
Build-time, hot in developmentThe file is read when the storefront bundle is compiled. In development the dev server watches it: save the file and the storefront recompiles and reloads, no restart. In production, run the build again.

Widgets are not part of this file, they already have placements in theme.json. Components rendered inline through an Area's coreComponents prop have no key and cannot be moved this way.

Activating a Theme​

To activate a theme, set the system.theme value in your configuration file to the theme's folder name:

config/default.json
{
"system": {
"theme": "yourtheme"
}
}

Editing config/default.json by hand only flips which theme renders. It does not install the theme's content. Prefer the CLI command:

npx evershop theme:active

theme:active does considerably more than update the configuration. In order:

  1. Resolves the theme id — from a positional argument, or interactively from the themes/ directory. The directory must exist; a typo aborts before anything is written.
  2. Reads and validates theme.json — SemVer version, well-formed widgets and placements, v4 UUIDs, cross-record references. Any validation error aborts activation; the active theme is left untouched. A theme with no theme.json is treated as presentation-only and skips to step 5.
  3. Installs or upgrades widgets and placements — a fresh install, or a version-gated upgrade that preserves merchant customizations and reports the conflicts it kept. Downgrades are refused.
  4. Provisions metafield definitions — from metafieldDefinitions[], idempotently. Runs on every outcome except a refused downgrade, and again on every server boot.
  5. Writes config.system.theme into config/default.json, then offers to run npm run build.
ArgumentEffect
<theme-id> (positional)Activate this theme without the interactive picker. Must match a directory in themes/.
--dry-runReport the pending content changes (added / updated / removed widgets and placements, conflicts, declared metafield definitions) and exit. Never writes the config or touches the database.
--content-onlyInstall the theme's content but leave the active theme unchanged. Skips the config write and the build prompt. Intended for CI provisioning and e2e setup.
-y, --yesSkip the post-activation "run npm run build?" prompt. Also skipped automatically when stdin is not a TTY.
# Preview what activating would change
npx evershop theme:active yourtheme --dry-run

# Install content in CI without switching the active theme
npx evershop theme:active yourtheme --content-only
Passing arguments through npm

With the npm script wrapper, arguments must come after --, or npm swallows them and a flag can be mis-read as the theme id:

npm run theme:active -- yourtheme --content-only
info

theme:status, theme:uninstall and theme:export-content are the companion commands for inspecting installed content, removing it, and exporting the current database state back into a theme.json.

src vs dist Requirements​

  • Development mode (npm run dev): EverShop compiles TypeScript on the fly. Your theme must have a src/ directory.
  • Production mode (npm run start): EverShop loads pre-compiled JavaScript. Your theme must have a dist/ directory. Run npm run build in your theme directory before building the project.
warning

After changing or updating a theme, you must rebuild your project (npm run build) for the changes to take effect.

Theming Utilities Commands​

Follow the tutorial to learn how to use theming utilities commands to speed up your theme development: