BizzPocket

The theme: theme.config.ts, layouts and styles

On this page

A theme is a folder. Blocks are one part of it. This page covers the rest: the name, the colours and fonts, the one-tap looks sellers can choose, the starter text for a new store, and the default page layouts.

my-theme/
  theme.config.ts        name, tokens, styles, verticals, starter content   (this page)
  package.json           name, version and the "./styles" export
  blocks/<type>/         one folder per block                              (first-block.md)
  components/            shared pieces: copied from the registry, or your own
  layouts/<page>.json    the default blocks of each page                  (this page)
  styles/global.css      optional CSS for the whole theme
  bizzpocket.json        what you copied from the registry                (registry.md)

There is no manifest.ts and no index.ts. The build reads theme.config.ts and the blocks/ folders and writes everything the storefront needs to dist/.


theme.config.ts

ts
import { theme } from '@bizzpocket/theme';

export default theme({
  id: 'my-theme',
  name: 'My Theme',
  tokens: { /* colours, fonts, ... */ },
  /* everything below is optional */
});

theme() returns the config you give it. It checks one rule for you: every token a style sets must also be in overridable.

Write id as a string literal (id: 'my-theme'), not a variable. The build reads it without running your code, and stops with THEME_ID_MISMATCH if it cannot find the same value. The id is lowercase with dashes.

The theme's version is the version in package.json. It must be a semantic version such as 0.1.0. The build stamps it on every layout. Never write it in the layout files.

A complete example. Create theme.config.ts:

theme.config.ts
import { theme } from '@bizzpocket/theme';

export default theme({
  id: 'my-theme',
  name: 'My Theme',

  // Design tokens. They become CSS variables: --colors-primary, --fonts-body, --spacing-l ...
  tokens: {
    colors: {
      primary: '#111111',
      'primary-foreground': '#ffffff',
      background: '#ffffff',
      foreground: '#1a1a1a',
      accent: '#2e7d23',
    },
    fonts: { body: 'system-ui, sans-serif', heading: 'Georgia, serif' },
    spacing: { s: '0.5rem', m: '1rem', l: '2rem' },
    radii: { card: '8px' },
  },

  // Only these tokens may be changed by a store's brand settings or by a style.
  overridable: ['colors.primary', 'colors.primary-foreground', 'colors.accent', 'colors.background'],

  // Store types this theme suits: used for "recommended for your store".
  verticals: ['general', 'toys'],

  // One-tap looks. The first is the default.
  styles: [
    {
      id: 'classic',
      name: 'Classic',
      description: 'Black and white',
      swatch: ['#111111', '#ffffff', '#2e7d23'],
      tokens: { 'colors.primary': '#111111', 'colors.background': '#ffffff' },
    },
    {
      id: 'bubblegum',
      name: 'Bubblegum',
      description: 'Berry pink on blush',
      swatch: ['#9d174d', '#fff7fb', '#6d28d9'],
      tokens: { 'colors.primary': '#9d174d', 'colors.background': '#fff7fb', 'colors.accent': '#6d28d9' },
    },
  ],

  // Words and pictures a brand-new store starts with.
  starterContent: {
    default: {
      home: {
        '6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a1002': { title: 'Our best sellers' },
      },
    },
    toys: {
      home: {
        '6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a1002': { title: 'Toys they will love' },
      },
    },
  },

  // The saved home layout is the only source of home page content.
  homeContent: 'layout',
});

Every option

Option What it is
id The theme's id: lowercase, dashes only, a string literal. Required.
name The name sellers see. Required.
tokens Colours, fonts and other design values. Required. See Tokens.
overridable The list of token paths a seller's brand settings or a style may change. See Tokens.
styles The one-tap looks. See Styles.
verticals Store types the theme suits. See Verticals.
starterContent Day-one text and pictures. See Starter content.
homeContent 'layout' or 'legacy'. See Home content.
publisher { id, name } of the author. Defaults to BizzPocket.
cdn Where images are served from: { kind, baseUrl, template?, signed? }. kind is one of bizzpocket, cloudflare, cloudinary, imgix, bunny, generic.
iconSet { kind: 'sprite', url }: an SVG sprite for icons.
image Image delivery defaults: defaultQuality (1 to 100), maxWidth, preferredFormats ('avif', 'webp', 'jpeg'), connectionAware.

Tokens

Tokens are the theme's design values. Each one becomes a CSS variable named --<group>-<name>, so colors.primary is var(--colors-primary) in your CSS.

css
.promo { background: var(--colors-primary); color: var(--colors-primary-foreground); font-family: var(--fonts-body); }

Groups: colors and fonts (both required), and optional spacing, shadows and radii. Token names use letters, digits, dashes and underscores only (primary-foreground is fine). Values are plain CSS values.

For a dark mode, give two sets instead of one:

ts
tokens: {
  light: { colors: { background: '#ffffff', foreground: '#111111' }, fonts: { body: 'system-ui' } },
  dark:  { colors: { background: '#111111', foreground: '#f5f5f5' }, fonts: { body: 'system-ui' } },
},

overridable is a safety list. A store owner can change brand colours, but only the tokens you list here. Anything else is ignored. Keep it narrow: brand colours, the surfaces they sit on and the type. Do not list tokens that could break your layout (spacing, radii, shadows).

Token paths are written group.name, such as 'colors.primary'.


Styles

A style is a one-tap look sellers can pick without editing anything: a name, a picture of its colours, and a set of token values.

Key Meaning
id A short unique id.
name, description Shown in the style picker.
swatch 3 or 4 hex colours for the picker chip.
tokens Token paths to values, such as { 'colors.primary': '#9d174d' }.

Rules:

  • At most 5 styles. The first is the default.
  • Every token a style sets must be in overridable. If not, theme() throws: style "bubblegum" sets token "colors.primary", which is not in the theme's overridable list.
  • Keep the text readable: check the contrast of text on background in every style.

Verticals

verticals lists the kinds of store the theme suits. The seller gallery uses it for "recommended for your store". The allowed values are:

jewellery, fashion, beauty, home, food, grocery, electronics, toys, handmade, health, general.


Starter content

A brand-new store has no words or pictures. starterContent gives the blocks in your default layouts some. It is only ever applied to default layouts, never to a layout a seller has saved.

The shape is vertical → page → block id → props:

ts
starterContent: {
  default: {                                  // any store
    home: {                                   // the home page
      '6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a1002': {   // the "id" of a block in layouts/home.json
        title: 'Our best sellers',
      },
    },
  },
  toys: { /* the same, for stores whose vertical is "toys" */ },
},
  • The first level is default or one of the verticals above.
  • The second level is a page type (home, product, shop...).
  • The third level is the id of a block in that page's layout file, with the props to set.

Home content

homeContent says who owns the home page's content.

  • 'layout': the saved home layout's block props are the single source. Blocks read their own words, pictures and picks from their props, and "hide a section" is the layout's hidden flag. Use this for every new theme.
  • 'legacy' (or leave it out): the old home-page form in the seller app still drives the home page.

Layouts: layouts/<page>.json

A layout lists the blocks of one page, in order. The file name is the page: home.json, product.json, shop.json, cart.json... A theme only needs the pages it wants to provide. The file name must be one of the page types listed below (or global). Any other name stops the build with INVALID_MANIFEST.

json
{
  "schemaVersion": 1,
  "pageType": "home",
  "pageIdentifier": "home",
  "themeId": "my-theme",
  "blocks": [
    {
      "id": "6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a1002",
      "type": "featured-collection",
      "props": { "title": "Best sellers", "count": 4 },
      "visibility": { "devices": ["mobile", "desktop"] }
    }
  ]
}
Key Meaning
type The block's folder name.
id A unique id for this block on this page. Starter content and saved edits refer to it.
props The starting values. Missing ones use the field defaults.
visibility.devices "mobile" and/or "desktop".
hidden true keeps the block in the layout but not on the page.
children Blocks inside a container block.

Do not put themeVersion in a layout. The build adds it from package.json.

The page types are: home, product, shop, category, collection, cart, checkout, account, orders, order_detail, wishlist, search, policies, login, signup, not_found, order_return, order_cancel, blog, blog_detail, content.

The global layout: layouts/global.json

Blocks that appear on every page, such as the header and footer, live in layouts/global.json:

json
{
  "schemaVersion": 2,
  "themeId": "my-theme",
  "themeVersion": "0.1.0",
  "regions": {
    "header": [{ "id": "6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a3001", "type": "header", "props": {} }],
    "footer": [{ "id": "6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a3002", "type": "footer", "props": {} }]
  }
}

If a theme has no global.json, the build tries to build one from the header and footer blocks in layouts/home.json. If it cannot find both, it stops with NO_GLOBAL_LAYOUT.

Block roles

A page needs certain blocks to work, such as a product page needs a product detail block. A block says which job it does with role (for example role: 'product-detail' or 'cart'). The build prints a warning if a layout is missing a block with a role the page expects. Roles: header, footer, product-detail, cart, checkout, login, signup, account, order-list, order-detail, wishlist, policy, search-results, not-found, order-return, order-cancel, blog-list, blog-post, content.


CSS

  • A block's own CSS: import './my-block.css' in the block file. It loads only on pages that use the block.
  • Theme-wide CSS: styles/global.css. It loads on every page, before the blocks' CSS. On the Bun storefront it is inlined in the page while it is small (up to 14 KB gzip, about the first network round trip) and served as a cacheable <link> stylesheet above that (createServer's inlineGlobalCssMaxGzipBytes changes the limit). Keep it small; put block styles next to their block.
  • package.json must export the stylesheet the build produces:
json
"exports": { "./styles": "./dist/styles.css" }

bizzpocket init already includes this. Without it the build stops with STYLES_EXPORT_MISSING.

Next: registry.md.