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 five 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
Landing page templates (JSON) theme/landing-templates/*.json Ready-made landing pages offered in the dashboard's "New landing page" picker — see Landing page templates
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)
├── landing-templates/        # OPTIONAL ready-made landing pages (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:

  1. Find its component in src/themes/custom/ (widget name ≈ file name: bannersbanners-section.tsx, categoriescategory-slider.tsx, …).
  2. Restyle it — Tailwind utilities preferred. Keep the props interface intact; render every prop the platform passes (images, links, settings like col/gap, colors from usePrimaryColors()).
  3. 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 sections product.related, category.grid, search.grid. Saved to that widget's settings.*.

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 own x-<slug>-* custom widgets).
  • For your own x-<slug>-* widgets, embed the full data payload (start from the widget's defaults, 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 from defaults at seed time as a safety net, but explicit data keeps everything in sync from the start.)
  • Multi-column layouts use the row container: { "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.json complete, 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 under assets/

6. Submission → review → live

  1. Submit dist/theme.zip through the partner panel. The platform validates the package automatically and rejects hard failures immediately.
  2. Review: the submission is manually reviewed (code + design).
  3. Approval triggers two automatic steps:
  4. your theme's seed is materialized — a template shop is created from your templates/*.json, your header-footer/*.json presets become the shop's default header/footer (first file of each kind wins), and your widget definitions are registered — and
  5. 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).
  6. 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