Build a theme with AI¶
You can build a complete, submit-ready BitCommerz theme with an AI coding agent
(Claude Code, Cursor, or any agent that can read files and run commands). The
hard part is not writing components — it is knowing the rules that make a theme
survive integration into the storefront. Those rules are not discoverable from
the kit alone, and a green npm run build does not prove a theme is correct.
This page gives you the whole ruleset as a skill file you can drop into your agent, plus the workflow to drive it.
Why a skill file rather than a prompt
The rules below encode failure modes we have actually shipped and fixed — themes that built clean, passed review, and then rendered unstyled or with broken images on a real shop. An agent that has not been told about them will reproduce them. Paste the skill once; it applies to every session.
1. What you need¶
| The theme kit | The BitCommerz Theme Preset Kit — your starting point. Every theme is a restyled copy of it. |
| A design | Ideally HTML mockups + screenshots, desktop and mobile, per page. Screenshots matter: they are the visual truth the agent checks itself against. |
| Node 20+ | To run npm run validate / build / package. |
| An AI agent | Claude Code is what the skill is written for; the content works in any agent. |
Read Theme Development first. This page assumes you know what the kit is and what the 43 required components are.
2. Install the skill¶
Claude Code — save the file below as:
~/.claude/skills/bitcommerz-theme/SKILL.md
It is then picked up automatically whenever you ask for theme work. Project-scoped
alternative: .claude/skills/bitcommerz-theme/SKILL.md inside your theme repo, so
it travels with the project and your teammates get it too.
Any other agent — paste the body (everything after the --- front-matter
block) into your system prompt, .cursorrules, AGENTS.md, or equivalent.
3. Drive it¶
Point the agent at your design and let it work in waves. A sequence that works:
Build a BitCommerz theme called "Aurora" (slug: aurora) from the designs in
./design/. Desktop and mobile mockups are in there per page, each as
code.html + screen.png.
Then, between waves, you run the gates — not the agent, because parallel agents race on the build:
cd aurora/theme-kit
npm run validate # warnings are OK
npm run build # 0 TypeScript errors REQUIRED
npm run package # → dist/theme.zip
The agent should work in this order. Steps 3–5 parallelise well (one agent per page group, disjoint files); everything else is serial.
- Scope — theme name/slug, design sources, currency locale.
- Foundation —
theme.config.json, the shared module (<theme>-shared.tsx) with the palette, fonts and<Icon/>, and<ThemeHead/>wired intoheader.tsx. - Port the designed pages — restyle only, preserving every export, prop and data-wiring.
- Portability sweep — grep out every token-named class. A palette swap does not fix these.
- Fidelity pass — compare each page against its
screen.pngand close layout/spacing/type gaps. - Gate — validate, build, package.
The single most important thing on this page
tsc and npm run build cannot catch the portability failures. They pass
green while the theme is broken — the breakage only appears after a merchant
activates it with real data. The grep audit in the skill is what catches them.
Run it after every wave and require it to be empty.
4. Submitting¶
Once dist/theme.zip exists, the normal pipeline applies — see
Post-Submit Pipeline & Checklist. Nothing about
building with AI changes review, approval or integration.
Shipping a fix to an already-published theme: resubmitting the same slug updates it in place. Bump the version, package, submit, and the platform re-materialises your widget defs and templates.
5. The skill file¶
Everything below the divider is the file. It is the same ruleset we use internally, with internal-only paths removed.
Two checks in the audit are internal-only
The Law 6 loop diffs your components against the storefront's default theme, and Law 7 references the platform repo. You do not have those checked out. Substitute the settings tables in Theme Development — they list the settings each widget must honour. Everything else runs against your theme kit as-is.
---
name: bitcommerz-theme
description: >-
Convert any web design (HTML mockups + screenshots) into a submit-ready BitCommerz storefront theme,
or fix/audit an existing one. Use when the user wants to build, port, or convert a BitCommerz theme;
turn a design/mockup into a theme; restyle the preset kit; fix a theme that renders unstyled on
the storefront; run a theme portability or design-fidelity audit; or wire theme pages into the local
preview. Triggers: "build a BitCommerz theme", "convert this design to a theme", "port the theme",
"theme renders unstyled", "theme fidelity check", "make a storefront theme".
---
# BitCommerz Theme Builder
Convert a design into a submit-ready BitCommerz theme (`dist/theme.zip`).
## Mental model
A theme is a self-contained copy of the preset kit whose components you restyle to a design.
The storefront **compiles the raw `.tsx` at integration with ITS OWN Tailwind — it ignores the
theme's config, tokens, fonts, and styles.css.** So the components must be **fully self-contained in
their styling** or they render unstyled once submitted. Pipeline:
author `theme-kit/` → validate/build/package → submit zip → approve → integrate → activate.
**Exception — the 7 product widgets own NO geometry.** `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 storefront layout. In the kit
their `src/themes/custom/<widget>.tsx` files are one-line re-exports of that shared layout — do NOT add
geometry to them. You control how these widgets look ONLY through `product-card.tsx` + the
product-card style contract (Law 6c). Their layout/settings
(`col`/`gap`/`limit`/`productLimit`/`isSlider`/`showButton`, trending's 4-left/image-center/4-right)
are the shared layout's job, identical on every theme.
## THE PORTABILITY LAWS (the whole game)
1. **Colors/sizes = arbitrary Tailwind literals with real hex/px** — `bg-[#700053]`, `text-[64px]`,
`leading-[72px]`. NEVER a config-token/theme-named class (`bg-primary`, `text-on-surface`,
`font-display`, `bg-magenta`, `text-ink`, `bg-cream`). Layout utilities (flex/grid/gap/aspect/
rounded-[..]) are fine.
2. **Fonts = inline** `style={{ fontFamily: THEME_SERIF }}`, loaded once by `<ThemeHead/>` (rendered at
top of `header.tsx`). Never add a font `<link>` in a component.
**Merchant override:** write the constants as
`export const X_SERIF = "var(--shop-font-heading, '<your stack>')"` (and `--shop-font-body` for the
sans). The storefront declares those vars — and loads the Google font — ONLY when the merchant
picked one in *Appearance → Typography*; a per-section Font control re-declares them on that block.
Unset ⇒ var undeclared ⇒ CSS falls back to YOUR stack, so the theme's typography survives. Keep
shipping your own font link.
**CRITICAL — `<ThemeHead/>` alone is NOT enough.** A shop can replace the theme's `header.tsx`
with a CUSTOM header block (dashboard header/footer builder — many shops do, and theme presets
seed one). Then ThemeHead never mounts and EVERY heading silently falls back to Georgia/system
(CSS asks for the family; the `<link>` was never added). An `Icon`-only hook doesn't save you —
a page may render no icons. **Fix: call `ensure<Theme>Fonts()` at MODULE SCOPE in
`<theme>-shared.tsx`** (guarded by `typeof document !== "undefined"`), so importing ANY component
of the theme injects the font link + global CSS.
3. **Icons = shared `<Icon/>`** from `<theme>-shared` (a webfont, e.g. Material Symbols). Never
`lucide-react` or external icon/font packages.
4. **Image src = always `getS3ImageUrl(...)`** — any field holding a raw S3 key (`product.thumbnail`,
`gateway.image`, banner/logo fields) must be wrapped before `<Image src=...>`. A raw key crashes
with `Failed to construct 'URL': Invalid URL` — but only once real data (not empty/mock props)
reaches it, so build/preview both stay green while it's broken. Check `cart/cart-design.tsx` +
`checkout/checkout-design.tsx` first — this is exactly where it bit every marketplace theme.
**4b — same for CUSTOM WIDGET media fields**: the dashboard's `media`/`bg-image`/`image-list`
pickers store shop-relative S3 keys → any widget rendering `s.image`/`v.poster`/`v.videoUrl` raw
(CSS `url('…')`, `<img>`, `<video>`) 404s the moment the merchant picks an image, while absolute-URL
defaults keep the preview green.
Pattern: `export const themeMedia = (src?: string) => src ? getS3ImageUrl(src, "MEDIA") : "";` in
`<theme>-shared.tsx`, used by every widget.
5. **Custom-widget data complete + mirrored** — (a) `widget.json` `defaults` carries a real,
presentable value for EVERY field (never `image: ""` — use curated Unsplash URLs as placeholders);
(b) component per-field fallbacks (`s.x || …`) EQUAL those defaults (dashboard back-fills empty
instances from defs — drift = merchant edits blind); (c) `templates/*.json` instances of your
`x-<slug>-*` widgets embed FULL `data` (defaults copy, page-tailored) — never `"settings": {}`,
which seeds merchant pages whose builder panels are blank while the storefront renders fallbacks
("theme not compatible with builder").
**(d) `categories` and `brand-list` in a template need an explicit `source`** — both
normally render a selection the MERCHANT picked, which a theme cannot make: you author
`templates/home.json` before the shop exists, so there are no category/brand ids to name. Ship
`"data": { "settings": { "source": "all", "limit": 6 } }`. `"all"` = the shop's own, capped by
`limit` (default 12); `"manual"` = the pick only. Omitting `source` still fills the section — the
platform infers (saved pick ⇒ manual, none ⇒ all, for pages built before the setting existed) — but
the builder's "Categories to show" dropdown then reads blank, as if the section were unconfigured.
6. **Honor the merchant's widget settings** — a canonical widget's controls are a CONTRACT the
theme must implement. Three halves:
(a) **Block style** (bgColor / padding / bg-image / boxed / customCSS — offered on EVERY widget)
is applied by the platform — themes must NOT re-apply it (double padding) and
must not spread `settings` into a style object.
(b) **Per-widget content/layout controls ARE the theme's job — for the widgets you still own**:
`uiType` (categories: Circle/Square/Slider), `isSlider`, `title`, `containerType`… A restyle
that drops one leaves the merchant clicking a control that does nothing. NOTE: the 7 product
widgets (see Mental model exception) are NOT your job.
(c) **Product-card style contract** — `product-card.tsx` must call
`useProductCardStyle(settings)` (exported from `../imports`) and apply the result as INLINE
OVERRIDES on top of its Tailwind classes: `card.bg/.border` (wrapper), `title.color`,
`price.color/.saleColor`, `addToCart.bg/.text/.hoverBg`, `buyNow.*`, `discountTag.bg/.text`.
Behind it are 11 merchant keys — `cardBgColor`, `cardBorderColor`, `titleColor`, `priceColor`,
`salePriceColor`, `addToCartBgColor`, `addToCartTextColor`, `buyNowBgColor`, `buyNowTextColor`,
`discountTagBgColor`, `discountTagTextColor` — set theme-wide in dashboard *Online Shop →
Appearance → Product cards* and/or per section in the builder's Layout tab.
**Every value is `string | undefined`, and `undefined` MUST stay a no-op** — never
`cardStyle.card.bg || "#fff"`; that flattens the theme's own design the moment you wire it.
Gotcha: a Tailwind `hover:bg-*` class cannot beat an inline `backgroundColor`, so if you set an
inline button bg you must drive hover from `hoverBg` too (shared `AnimatedButton` takes
`bgColor`/`textColor`/`hoverBgColor` and handles it); if nothing is set, leave `hover:*` alone.
7. **Don't rebuild a canonical widget — and never hardcode an announcement bar.** Before writing a
custom `x-<slug>-*` widget, check `schemas/canonical-widgets.json`; if a canonical widget already
covers it, use that. A merchant keeps a canonical widget's content when they switch themes and
loses a theme-scoped one's.
**The specific trap: the top announcement/promo strip.** Themes routinely hardcode one into
`header.tsx` — merchants then cannot edit the text, the link or the colors, and cannot turn it off.
There is a canonical **`announcement-bar`** widget: message, optional link (empty link text ⇒ whole
bar clickable), background/text color, align, `compact|normal|tall` height, sticky, and dismissible
(remembered per shopper, keyed by message text). It takes **no** `containerType`/padding — it is
edge-to-edge by definition.
So: **do not put a promo strip in `header.tsx`.** Two merchant-editable homes, depending on where
the design puts the bar:
- **Above the site header** (the usual design) → the header builder's `announcement` config. Three
columns (`elements.left/center/right`) taking `text` (with `flash|fade|slide-up` motion),
`rotate` (text slider), `marquee`, `phone` and `social` elements, plus colors,
`compact|normal|tall` and sticky. Ship it in your `header-footer/sample-header.json` preset so
the merchant inherits your design and can edit it. All motion is CSS keyframes — compiled headers
carry no JS, which is also why there is no close button.
- **At the top of the page body**, or when it must be dismissible → the canonical
`announcement-bar` page widget, dropped first on the page.
`tsc`/`npm run build` **cannot** catch a law-1/4/5/6 violation — it passes green while the theme is
broken. **Only a grep for token-NAMED classes (law 1), unwrapped `<Image src=` (law 4), unwrapped
widget media fields (law 4b), empty defaults/template data (law 5), or settings the default theme
reads and yours doesn't (law 6) catches it.** This is the #1 failure mode.
Import depth for `<theme>-shared`: top-level `custom/*.tsx` → `./` · one-level subdir → `../` ·
`widgets/x-*/` → `../../`.
## Workflow
1. **Scope** — confirm design source (`design/*/{code.html,screen.png}` desktop+mobile), theme
name/slug, currency locale. Build a per-theme design→hex cheat-sheet in the theme's `HANDOFF.md`.
2. **Foundation** (serial) — `theme.config.json` (name/slug/colorSchemes/fonts), `tailwind.config.js`
(local-preview only), `<theme>-shared.tsx` (`<ThemeHead/>`, `Icon`, palette + font consts), wire
`<ThemeHead/>` into `header.tsx`. Palette-substitute hexes across components as a baseline.
3. **Port designed pages** (PARALLEL — one agent per page, disjoint files): each reads its
`design/*/code.html` and restyles the component(s), **preserving every export/prop/data-wiring —
restyle only**. Groups: category(4) · PDP(5) · checkout(3) · order-confirmation(2) · blog-list(2) ·
blog-detail(1) · home(widgets).
4. **Portability sweep** (PARALLEL) — grep every remaining token-NAMED class and the "undesigned"
contract pages (auth/user/store-locator/pre-order/request-order/cart) and convert. A palette swap
does NOT fix token classes. Fix old-brand wordmark text + currency here too.
5. **Fidelity pass** (PARALLEL — one agent per page) — each **reads the design `screen.png`** (visual
truth) + `code.html` and fixes layout/spacing/type/detail gaps.
6. **Gate centrally** (YOU, never the agents — they race on build): after each wave run the grep audit
+ `npm run validate` + `npm run build` (0 TS errors) + `npm run package`.
## Agent brief template (for steps 3-5)
Give each subagent: the design file paths (screen.png for fidelity), the target component file list,
the portability laws, the import-depth rule, the design→hex cheat-sheet, "preserve every
export/prop/data-wiring — restyle only", "do NOT run npm build/package", and "grep your files clean of
token classes when done". One page group per agent; groups are disjoint so they run concurrently.
## Grep audit (run after every wave — MUST be empty)
```bash
cd <theme>/theme-kit/src/themes/custom
grep -rnE '\b(bg|text|border|ring|fill|placeholder)-(primary|secondary|tertiary|on-surface|on-primary|on-background|<OLD_THEME_COLOR_NAMES>)[a-z0-9-]*|font-display|<old-utility-classes>|aspect-product' --include='*.tsx' . \
| grep -vE '<theme>-(marquee|fade|zoom|dropcap|hover|editorial)' # exclude your real util classes
grep -rniE '<old-brand-name>|<old-font-family>' --include='*.tsx' . # brand/font leftovers
# Law 4 — every <Image src=...> must be wrapped in getS3ImageUrl(...); cart/checkout carry the
# highest risk (product thumbnails, gateway logos):
grep -rn '<Image' -A2 cart/cart-design.tsx checkout/checkout-design.tsx | grep 'src=' | grep -v getS3ImageUrl
# Law 4b — widget media fields must go through the themeMedia/getS3ImageUrl resolver:
grep -rnE "url\('\\\$\{[a-z]+\.(image|poster|src|bgImage)" widgets/ | grep -viE 'themeMedia|getS3ImageUrl'
grep -rnE 'src=\{[a-z]+\.(videoUrl|poster|image)\}' widgets/ | grep -viE 'themeMedia|getS3ImageUrl'
# Law 5 — no empty image defaults in widget defs; no empty data in template instances of x-* widgets:
grep -rn '"image": ""\|"poster": ""\|"src": ""\|"bgImage": ""' widgets/*/widget.json
python3 -c "
import json,glob
for f in glob.glob('templates/*.json'):
for w in json.load(open(f)).get('widgets',[]):
if w['name'].startswith('x-') and not ((w.get('data') or {}).get('settings')):
print(f, w['id'], 'EMPTY DATA')"
# Law 5d — categories / brand-list seeded without an explicit source (blank builder dropdown):
python3 -c "
import json,glob
for f in glob.glob('templates/*.json'):
for w in json.load(open(f)).get('widgets',[]):
if w['name'] in ('categories','brand-list'):
s = ((w.get('data') or {}).get('settings') or {})
if 'source' not in s:
print(f, w['id'], w['name'], 'MISSING settings.source (use \"all\")')"
# Law 6c — product-card style contract wired, and no default substituted for an unset value:
grep -q 'useProductCardStyle' product-card.tsx || echo 'product-card.tsx MISSING useProductCardStyle'
grep -nE 'cardStyle\.[a-zA-Z.]+ *(\|\||\?\?) *["'"'"'#]' product-card.tsx # must be empty
# Law 7 — a promo/announcement strip hardcoded into the header. Heuristic, so read the hits:
grep -rniE 'free (delivery|shipping)|orders over|announcement|top-?bar|promo.?bar' header.tsx \
&& echo '^^ move this to the canonical announcement-bar widget'
ls -d widgets/x-*-{topbar,announcement,announce,promo-bar} 2>/dev/null \
&& echo '^^ theme-scoped top bar — use the canonical announcement-bar instead'
```
## Gates
```bash
cd <theme>/theme-kit
npm run validate # warnings OK ("styles directory missing" is a dead check)
npm run build # 0 TS errors REQUIRED (build downgrades errors to warnings — eyeball output)
npm run package # → dist/theme.zip
# preview: ./node_modules/.bin/webpack serve --mode development --port 3000
# NOT `npm run dev` (serves a non-existent bundle → {error:"Internal server error"})
```
## Contract (validator + integrate check)
43 required components with exact filenames/paths/exports. Custom widgets in
`widgets/x-<slug>-<name>/{index.tsx,widget.json}` registered in `widgets/index.ts`; widget component
signature `({ widget:{id,name,data} }) => …` rendering from `widget.json` defaults; repeater/media/
switch field types for bespoke sections (plus `code` for html/css fields). Templates in
`templates/*.json` use canonical or `x-<slug>-*` widget names.
**Landing page templates (OPTIONAL)** — `landing-templates/*.json`: ready-made landing pages the
theme ships, shown in the dashboard's "New landing page" picker while the theme is active. Shape
(widgets-v1): `{ name (required, = picker card + page name), description?, widgets[] (required),
pageSettings? }`. Widgets: canonical (esp. `custom-html` = `data:{html,css,settings}` raw-HTML block —
prefix CSS selectors, no `<script>` exec) or the theme's own `x-<slug>-*`. Ids are re-minted on use.
`npm run validate` checks name/widgets/unknown-widget-names. Authoring shortcut: build the page
visually in the dashboard, copy its `widgets` array into the JSON. An optional `preview` key (any
image URL, or a `data:image/webp;base64,…` URI so the theme stays self-contained) renders as the card
thumbnail in the picker.
**Single-product order form** — the canonical `landing-checkout` widget ("Order Form (Checkout)")
renders product + on-page checkout (form, shipping, COD/gateway, OTP, submit) for single-product/COD
landing pages. Data: `{ products:[{id,slug}], settings:{ containerType, showAdditionalDetails } }`. It
reuses YOUR theme's `checkout/checkout-design.tsx`, which must honor these optional props on landing
(the kit's checkout-design already does — keep its `isLandingPage` blocks when restyling):
`isLandingPage`, `showAdditionalDetails`, `onUpdateQuantity(skuId,qty)`,
`onRemoveFromLandingCart(skuId)`.
**Optional theme-overridable modules** — the storefront falls back to its default theme when yours
lacks `services`, `brand-list`, `button`, `quick-view-section`, `random-banners`. Fallback =
default-theme styling mid-page + a "Cannot find module" red overlay in the merchant's builder preview.
**Ship your own component for any of these used by your templates** (home templates almost always use
`services`). The kit ships a restylable `services.tsx` — restyle it, never delete. Kit gotcha: the
`services` widget's items live at `data.items` (root), NOT `data.settings.items` — shaped
`{title,description,icon,iconType}`.
## Pitfalls
- Palette (hex→hex) swap does NOT fix token-NAMED classes — always grep token names.
- Radius tokens are small: design `rounded-full` ≈ 0.75rem → `rounded-[12px]`, not a pill.
- `<ThemeHead/>` only mounts via `<header>` — headerless/preview renders need it mounted explicitly.
- `imports.tsx` container stubs render only `{children}` — PDP preview composes the section components
directly.
- Currency: `${x}` is a JS template literal; only `$`+`{x}` is currency — normalize carefully.
- True storefront render happens only after submit→approve→integrate.
- Keep a living per-theme `HANDOFF.md` (state + cheat-sheet + deliberate divergences); resume from it.
- Image fields (Law 4) crash only with **real** data — empty/mock props hide the bug completely, so a
green build/preview proves nothing here. Grep, don't trust the build.
- Post-purchase symptoms decode: broken/404 images the merchant set = Law 4b (raw media keys);
blank builder settings panels while the storefront shows content = Law 5c (empty template data);
a mid-page section suddenly default-styled + "Cannot find module './<slug>/<name>'" in preview =
missing optional overridable module; **a per-widget builder control that does nothing = Law 6** —
the theme dropped the prop during restyle (the Categories "Style" select / `uiType` is the classic).
- Fix shipped? Resubmitting the SAME slug updates the theme in place: bump version → package →
submit → publish → the platform re-syncs and re-materializes widget defs + templates. Merchant
pages keep their data.
See also: Theme Development · Theme Portability & Fidelity Playbook · Custom Theme Widgets · Post-Submit Pipeline & Checklist