Theme Development Guide¶
Build a complete storefront theme for the Bitcommerz platform using the Theme Preset Kit. A theme controls how every storefront surface looks — header, footer, product page, category, cart, checkout, search, blog, user dashboard — while all business logic (cart math, checkout, auth, data fetching) stays in the platform.
A theme package can ship four kinds of things:
| What | Where in the package | What it controls |
|---|---|---|
Components (.tsx) |
theme/*.tsx, theme/<area>/… |
How each section/widget LOOKS |
| Default-page templates (JSON) | theme/templates/{type}.json |
Which sections show on system pages, in what order/layout |
| Custom widgets | theme/widgets/x-<slug>-<name>/ |
Brand-new builder widgets merchants can drag onto pages — see Custom theme widgets |
| Screenshots & static assets | screenshots/, theme/assets/ |
Marketplace preview images + banners/images the theme ships — see Theme screenshots & assets |
1. Getting started¶
The kit is a standalone React + TypeScript + Tailwind project that mirrors the platform's default theme 1:1 — same file names, same component APIs, same types.
cd theme-preset-kit
npm install
npm run dev # preview server with mock data → http://localhost:3000
npm run preview # webpack HMR dev server → http://localhost:8080
Edit theme.config.json first — it identifies your theme:
{
"name": "Aurora",
"slug": "aurora", // lowercase letters/numbers/hyphens; must be unique;
// must NOT be default | grid | manfare | general
"version": "1.0.0",
"author": { "name": "…", "email": "…" },
"description": "…"
}
The slug matters: it becomes your theme's identity across the whole platform
and the required namespace prefix for custom widgets (x-<slug>-…).
2. Theme structure¶
src/themes/custom/
├── index.ts # export barrel — every component the platform loads
├── header.tsx footer.tsx # chrome
├── product-card.tsx # used everywhere products render
├── single-product-page.tsx # product page (monolith fallback)
├── category-page.tsx search-page.tsx cart-page.tsx checkout-page.tsx
├── banners-section.tsx hero-section.tsx … # builder widget designs
├── auth/ cart/ checkout/ home/ order-confirmation/
├── pre-order/ request-order/ store-locator/ # area sub-designs
├── templates/ # default-page section layouts (JSON)
├── header-footer/ # sample header/footer presets (JSON)
├── assets/ # OPTIONAL bundled banners/images (@assets alias)
└── widgets/ # OPTIONAL custom widgets (x-<slug>-*)
screenshots/ # OPTIONAL marketplace thumbnail + preview images
Two OPTIONAL image folders exist alongside the code — bundled assets/ (banners
and images the theme renders, shipped in theme/assets/) and root screenshots/
(the thumbnail + preview images shown on the marketplace listing). Both are covered
in Theme screenshots & assets.
The component contract is fixed. Every file name, export, and prop
interface must match the platform's default theme — npm run validate checks
the required list. You change what's INSIDE the components, not their API.
Shared dependencies come from the import barrel:
import { Image, Link, getS3ImageUrl, ProductItemV2, usePrimaryColors } from "../imports";
In the kit, ../imports resolves to mock implementations so you can develop
standalone. When your theme is integrated into the storefront, the same import
resolves to the real platform modules — which is why the types must match
(never edit the type shapes in src/types/; they mirror the platform).
Always resolve image fields through getS3ImageUrl()
Product thumbnails, payment gateway logos, and similar fields hold a raw S3 key, not a URL. Passing
one straight into <Image src=...> crashes (Failed to construct 'URL': Invalid URL) — but only
once real data reaches it, so an empty preview and a green build both hide the bug. Always wrap:
<Image src={getS3ImageUrl(item.product.thumbnail)} .../>. See the
Portability Playbook
for the full rule and a grep to verify it.
3. Using & customizing the existing widget components¶
Merchants build pages from a fixed catalogue of widgets (slider,
banners, product-slider, populer-product, categories, title,
video-section, …) and default-page sections (product.gallery,
cart.summary, checkout.address, …). The full canonical list ships in the
kit at schemas/canonical-widgets.json.
A widget's data is theme-independent; your theme changes its look. When a
merchant drops a banners widget on a page, the storefront picks YOUR
banners-section.tsx while your theme is active. Same widget JSON, different
visual.
To customize an existing widget:
- Find its component in
src/themes/custom/(widget name ≈ file name:banners→banners-section.tsx,categories→category-slider.tsx, …). - Restyle it — Tailwind utilities preferred. Keep the props interface intact;
render every prop the platform passes (images, links, settings like
col/gap, colors fromusePrimaryColors()). - Check it in the preview (
npm run dev→ Components page shows each widget with mock data).
The 7 product widgets own no geometry — you cannot restyle their layout
trending-products, populer-product, new-arrival, product-slider
(featured-products.tsx), filter-products, three-banner-products, and
best-selling (delegates to trending) render through ONE shared layout in the
storefront (src/components/product/widgets/<widget>-layout.tsx). In the kit
their src/themes/custom/<widget>.tsx files are one-line re-exports of that
shared layout — every theme renders them identically (e.g. trending =
4 products left / feature image centered / 4 right). Do not add geometry to
these files. You customize how these widgets look ONLY through
product-card.tsx and the Product-card style contract below. This is what
keeps merchant widget settings working on every theme — the previous
per-theme copies silently dropped them.
Rules that keep your theme compatible:
- Never rename or remove a required component, its default export, or its props. Validation and integration both reject broken contracts.
- Respect merchant settings — see the box below; this is the single most common way a restyle silently breaks a theme.
- Use theme colors via the shared hooks (
usePrimaryColors,useButtonColors) so the merchant's Appearance settings apply to your theme.
Every builder control is a contract — a control that does nothing is a bug
Block style is handled for you. Background colour, padding, background
image, boxed width and custom CSS (the Advanced / Layout tabs, offered
on every widget) are applied by the storefront around your component.
Don't re-apply them (you'd double the padding), and never spread settings
into a style={{...}} object — that leaks non-CSS keys into the DOM.
Per-widget content and layout controls are YOUR job — for the widgets you still own. The container hands them to your presentation component as props/settings; if you drop one while restyling, the merchant changes it in the builder and nothing happens. Commonly missed:
| Widget | Control | Key |
|---|---|---|
| Categories | Style (Circle / Square / Slider) | uiType |
| Categories | Slider mode | isSlider |
| Banners / hero / most sections | Section title / subtitle | title, subtitle |
| Product cards | Show add-to-cart / buy-now / hide price | addToCart, buyNow, hidePrice |
| Product cards | Card / title / price / button / tag colors | see Product-card style contract below |
The 7 product widgets (trending-products, populer-product,
new-arrival, product-slider, filter-products, three-banner-products,
best-selling) are the exception: their col/gap/limit/isSlider/
productLimit/showButton controls are read by the SHARED storefront layout,
not your theme — you don't wire them, and you can't break them.
Before you package, diff a widget you DO own against the default theme's version and make sure you read the same keys:
bash
grep -oE 'uiType|settings\.[a-zA-Z]+' category-slider.tsx | sort -u
Product-card style contract¶
Merchants can recolor product cards without touching code, in two layers:
- Theme-wide — dashboard → Online Shop → Appearance → Product cards.
Saved to
theme-settings.settings.productSettings.*. - Per section — the same swatches in the page builder's Layout tab on
every product widget (
product-slider,populer-product,trending-products,new-arrival,filter-products,three-banner-products) and on the default-page sectionsproduct.related,category.grid,search.grid. Saved to that widget'ssettings.*.
Same eleven keys in both layers:
| Key | Colors |
|---|---|
cardBgColor, cardBorderColor |
card background, card border |
titleColor |
product title |
priceColor, salePriceColor |
price; the sale color applies to the live figure when the product is discounted |
addToCartBgColor, addToCartTextColor |
add-to-cart button (hover is auto-darkened 15%) |
buyNowBgColor, buyNowTextColor |
buy-now button |
discountTagBgColor, discountTagTextColor |
discount badge |
Your card does not resolve the layers itself — useProductCardStyle(settings)
does it (section → theme-wide → nothing) and hands you a ready object:
import { useProductCardStyle } from "../imports";
const ProductCard = ({ product, settings }: ProductCardProps) => {
const cardStyle = useProductCardStyle(settings);
return (
<div
className="bg-[#fcf9f2] border-2 border-[#1c1c18] rounded-xl"
style={{
backgroundColor: cardStyle.card.bg,
borderColor: cardStyle.card.border,
}}>
<h3 className="text-[#1c1c18] font-bold" style={{ color: cardStyle.title.color }}>
{product.name}
</h3>
<AnimatedButton
bgColor={cardStyle.addToCart.bg}
textColor={cardStyle.addToCart.text}
hoverBgColor={cardStyle.addToCart.hoverBg}>
Add to cart
</AnimatedButton>
</div>
);
};
An unset value must be a no-op — never substitute a default
Every field of the returned object is string | undefined. undefined
means the merchant chose nothing, and your theme's own hand-authored look
must survive untouched. So apply these as inline overrides on top of your
Tailwind classes — an inline backgroundColor: undefined changes nothing,
while a set value beats the class. Do not do
cardStyle.card.bg || "#ffffff": that flattens your design the moment the
hook is wired.
One consequence to watch: a Tailwind hover:bg-* class cannot beat an
inline backgroundColor. If you set an inline button background, drive the
hover from cardStyle.*.hoverBg too (the shared AnimatedButton already
does this via hoverBgColor). If you set nothing, leave the hover:*
classes alone.
The legacy per-widget key discountTagBGColor (capital BG) is still honored
by the hook, so old pages keep their tag color.
Typography — make your fonts merchant-overridable¶
The merchant picks a heading font and a body font (curated Google Fonts)
in Online Shop → Appearance → Typography, and can override the font of a
single block in the builder's Layout tab. The platform does the loading: the
storefront's root layout emits the Google Fonts <link> and declares
:root { --shop-font-heading: 'Lora', serif; --shop-font-body: 'Inter', sans-serif; }
only when the merchant actually picked one. Your theme opts in by writing its font constants with its own stack as the CSS fallback:
// <theme>-shared.tsx
export const MY_SERIF = "var(--shop-font-heading, 'Playfair Display', Georgia, serif)";
export const MY_SANS = "var(--shop-font-body, 'Plus Jakarta Sans', system-ui, sans-serif)";
Nothing else changes — components keep doing style={{ fontFamily: MY_SERIF }}.
When the merchant picks nothing the var is undeclared and CSS falls back to your
stack, so your design is untouched. A per-section override re-declares the same
vars on that block's wrapper, so it reaches these constants too.
Keep shipping your own font <link>
Your theme still loads its own fonts (the ensure<Theme>Fonts() /
<ThemeHead/> pattern) — that's what renders when the merchant picks
nothing. The platform only loads the families the merchant chose.
Bengali: a Latin-only family cannot render বাংলা. If your theme targets a Bengali storefront, pick a Bengali-capable stack (Hind Siliguri, Noto Sans Bengali) as your fallback.
Optional theme-overridable components (ship the ones your templates use)¶
Beyond the required contract set, the storefront looks for these per-theme
modules and silently falls back to the default theme's version when your
theme doesn't ship them: services, brand-list, button,
quick-view-section, random-banners.
The fallback keeps the page working, but the section renders in the default
theme's styling — a visible style clash in the middle of your design. Rule of
thumb: if a widget name appears in any of your templates/*.json, ship your
theme's own component for it (match the default theme's props contract and
restyle). The kit ships a restylable services.tsx — the most commonly hit,
since most home templates use it. Restyle it; don't delete it.
(new-arrival used to be in this list. It is now one of the 7 shared product
widgets above — its new-arrival.tsx is a re-export owning no geometry, so
there is nothing per-theme to restyle or clash. Style it via product-card.tsx.)
services items live at data.items, not data.settings.items
The services widget's repeater is rooted at data.items with items shaped
{title, description, icon, iconType} (iconType: "icon" uses the
built-in icon keys truck/headphones/refresh/shield; "image"
treats icon as a media key). Items placed under settings.items
silently render nothing.
4. Default-page templates¶
Your theme can ship the initial section layout for each system page in
src/themes/custom/templates/{type}.json (product, category, search, cart,
checkout, order-confirmation, wishlist, not-found — see
Default page builder for the section catalogue
and file shape):
{
"schemaVersion": 1,
"type": "product",
"name": "Product page",
"status": "published",
"widgets": [
{ "id": "gallery", "name": "product.gallery", "active": true, "data": { "settings": {} } },
{ "id": "buybox", "name": "product.buybox", "active": true, "data": { "settings": {} } }
]
}
- Section
names must come from the canonical catalogue (or be your ownx-<slug>-*custom widgets). - For your own
x-<slug>-*widgets, embed the fulldatapayload (start from the widget'sdefaults, then tailor per page) instead of shipping"data": { "settings": {} }— empty instances leave the merchant's builder settings panel blank while the storefront renders your component fallbacks, and the merchant can't edit what they see. (The platform back-fills empty data fromdefaultsat seed time as a safety net, but explicit data keeps everything in sync from the start.) - Multi-column layouts use the
rowcontainer:{ "name": "row", "data": { "settings": { "columns": 2, "columnWidths": ["2fr","1fr"] }, "cells": [[…],[…]] } }. - When a merchant applies your theme, these templates are copied into their shop and published — they become the starting point the merchant then edits in the dashboard builder.
Your theme can also ship header/footer presets in
src/themes/custom/header-footer/*.json ({config, content, styles} shape
from the header/footer builder — see
Header & footer builder). Files with "header" in
the name become headers, "footer" footers; the first of each kind is seeded as
the shop default and applied (and published to S3) together with your
templates. A theme without presets leaves the merchant's existing
header/footer untouched.
5. Validate, build, package¶
npm run validate # structure, component contract, template + widget-def checks
npm run build # TypeScript check + stage dist/theme
npm run package # validate + build + create dist/theme.zip (uploadable)
Validation gates (the same checks run server-side on upload, so fix locally):
- all required components present;
theme.config.jsoncomplete, slug valid - every template section name in the canonical contract (or your
x-<slug>-*) - custom widget folders namespaced and each shipping
widget.json+index.tsx - no banned code: Node built-ins, own network calls,
eval,dangerouslySetInnerHTML(the kit ships the same ESLint config —.eslintrc.themes.json) - declared screenshots/thumbnail present (warn if missing) and every
assets.*manifest entry resolves to a real file underassets/
6. Submission → review → live¶
- Submit
dist/theme.zipthrough the partner panel. The platform validates the package automatically and rejects hard failures immediately. - Review: the submission is manually reviewed (code + design).
- Approval triggers two automatic steps:
- your theme's seed is materialized — a template shop is created from
your
templates/*.json, yourheader-footer/*.jsonpresets become the shop's default header/footer (first file of each kind wins), and your widget definitions are registered — and - the storefront build integration compiles your components in
(
integration_status:pending_build → integrated; a build failure quarantines only your theme —build_failed— and you'll need to fix and resubmit). - Once
integrated+ seeded, your theme is purchasable: merchants buy/ activate it, the platform snapshots their current design, applies your templates, and renders your components. Merchants can restore their snapshot at any time, so applies are always reversible.
Versioning¶
Bump version in theme.config.json on every resubmission. Re-approval
re-runs the same seed + integration pipeline.
See also¶
- Theme Portability & Fidelity Playbook — read this before styling — how to keep components from rendering unstyled on the storefront, convert a design faithfully, and QA locally
- Custom theme widgets — add brand-new widgets to the merchant builder
- Theme screenshots & assets — marketplace preview images + bundled banners/images
- Default page builder (section templates)
- Header & footer builder