Getting started Stencil
Docs · Integration · Future-state

Catalyst migration

If USA Clean replatforms from Stencil to BigCommerce's Catalyst (Next.js + React + Tailwind v4), Caster comes along for the ride. The token contract stays identical — only the consumption layer changes. This guide covers the future-state setup so the eventual replatform is a config swap, not a redesign.

Status: hypothetical. No active Catalyst migration is in progress. This doc exists so the eventual move doesn't catch the design system off-guard.

1 · Set up Catalyst with Caster tokens

Catalyst is a Next.js 14+ app using Tailwind v4. Tailwind v4 reads its theme from CSS variables via the @theme directive — which is exactly what tokens.tailwind.css ships.

Vendor the tokens

your-catalyst-app/
├── app/
│   ├── globals.css                ← Tailwind entry; we'll edit this
│   └── ...
├── public/
├── styles/
│   └── caster/
│       ├── tokens.tailwind.css    ← from design-system/tokens/
│       └── components.css         ← from design-system/components/components.css
│                                       (only if you want Caster's component classes too)
└── tailwind.config.ts             ← may not exist in v4 — config moves to CSS

Wire it into globals.css

/* app/globals.css */

@import "tailwindcss";

/* Caster tokens — defines all theme variables via @theme block */
@import "../styles/caster/tokens.tailwind.css";

/* Optional: Caster component classes alongside Tailwind utilities */
@import "../styles/caster/components.css?v=0.16.3";

/* Your app-specific styles */
@layer components {
  /* ... */
}

That's it. Tailwind utilities now resolve through Caster tokens. bg-brand-default uses the brand blue, p-4 uses Caster's spacing.4 (16px), text-text-default uses the body text color.

2 · Using utilities (the React way)

In Catalyst, components are React components. Tailwind utilities replace most CSS:

Caster button as a React component

// components/Button.tsx

type ButtonVariant = 'primary' | 'brand' | 'outline' | 'ghost' | 'danger';
type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

const variants: Record<ButtonVariant, string> = {
  primary: 'bg-accent-default text-text-default hover:bg-accent-hover',
  brand:   'bg-brand-default text-text-inverted hover:bg-brand-hover',
  outline: 'bg-transparent text-brand-default border-2 border-brand-default hover:bg-brand-default/6',
  ghost:   'bg-transparent text-text-subtle border border-border-default hover:bg-surface-subtle',
  danger:  'bg-feedback-error-default text-text-inverted hover:bg-feedback-error-hover',
};

const sizes: Record<ButtonSize, string> = {
  xs: 'h-7 px-3 text-2xs',
  sm: 'h-9 px-4 text-xs',
  md: 'h-11 px-5 text-sm',
  lg: 'h-13 px-6 text-md',
  xl: 'h-15 px-8 text-md',
};

export function Button({
  variant = 'brand',
  size = 'md',
  children,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: ButtonVariant;
  size?: ButtonSize;
}) {
  return (
    <button
      className={`inline-flex items-center justify-center gap-2 rounded font-semibold whitespace-nowrap transition-colors ${variants[variant]} ${sizes[size]}`}
      {...props}
    >
      {children}
    </button>
  );
}

Notice that all the values resolve through Caster: bg-brand-defaultvar(--caster-color-brand-default)#0021a6. Re-themes still happen at the token layer.

3 · Two strategies for component coverage

Strategy A — Use components.css directly

Import Caster's components.css alongside Tailwind utilities. Use className="btn btn-primary" exactly as in vanilla.

  • Pro: zero rewrite. Identical markup as Stencil.
  • Pro: caster components stay maintained in one place.
  • Con: doesn't feel native to a React/Tailwind codebase.
  • Con: Tailwind's CSS isolation strategies (e.g., scoped @layer) get more complex.

Strategy B — Wrap Caster classes in React components (recommended)

Build idiomatic React components like the <Button> example above. They consume Caster tokens through Tailwind utilities. Reuse the same prop API across components.

  • Pro: idiomatic React + TypeScript. Type-safe variants.
  • Pro: tree-shakeable; unused styles never ship.
  • Con: more upfront work — every component needs a wrapper.
  • Con: two source-of-truth: Caster's components.css for Stencil, React components for Catalyst.
Recommendation: Strategy B for Catalyst-native development, with Strategy A as a fallback for components not yet wrapped. Both can coexist — components.css is just CSS, React imports it once and the classes are available everywhere.

4 · Server vs client components

Catalyst uses Next.js App Router. Caster components are pure presentational — they have no state, no event handlers — so they default to server components. No 'use client' needed.

// components/ProductCard.tsx — server component (default in App Router)

import { Button } from './Button';
import { StockBadge } from './StockBadge';

export function ProductCard({ product }: { product: Product }) {
  return (
    <div className="bg-surface-default border border-border-default rounded-md overflow-hidden flex flex-col">
      <a href={`/product/${product.slug}`} className="aspect-square bg-surface-subtle">
        <img src={product.image} alt={product.name} className="w-full h-full object-contain p-4" />
      </a>
      <div className="p-4 flex-1 flex flex-col gap-2">
        <div className="text-2xs text-text-muted">SKU · {product.sku}</div>
        <a href={`/product/${product.slug}`} className="text-xs font-semibold text-text-default hover:text-brand-default">
          {product.name}
        </a>
        <div className="mt-auto pt-2 border-t border-border-subtle flex justify-between">
          <div className="text-md font-bold">${product.price}</div>
          <StockBadge state={product.stockState} />
        </div>
      </div>
      <div className="px-4 pb-4">
        <Button variant="brand" size="sm" className="w-full">Add to cart</Button>
      </div>
    </div>
  );
}

For interactive components (Add-to-cart actions, qty steppers, search), wrap the interactive part in a child client component. The card itself stays server-rendered.

5 · Migration phases (when the time comes)

  1. Phase 1 — Tokens contract. Set up Catalyst app, import tokens.tailwind.css. All Tailwind utilities now use Caster values. No components migrated yet.
  2. Phase 2 — High-traffic React components. Build <Button>, <ProductCard>, <Pill>, <StockBadge> with Caster utilities. Visual parity with Stencil site.
  3. Phase 3 — Page templates. Build PLP, PDP, header, footer using the React components. Soft-launch behind a feature flag.
  4. Phase 4 — Long-tail components. Forms, account pages, checkout. Each gets a React wrapper.
  5. Phase 5 — Decommission Stencil theme. Once Catalyst hits feature parity and metrics hold, redirect shop.usaclean.com to the Catalyst app.

What survives the migration

Token names and semantic structure don't change. If a component on Stencil uses --caster-color-brand-default, the Catalyst version uses bg-brand-default (which resolves to the same value). When the brand re-themes, both sides update simultaneously.

AssetStencil consumptionCatalyst consumption
Brand color$caster-color-brand-defaultbg-brand-default
Text color$caster-color-text-defaulttext-text-default
Spacing$caster-spacing-4p-4
Component button.btn .btn-primary<Button variant="primary" />
Figma variablesTokens StudioTokens Studio (same JSON)

Caveats

Tailwind v4 specifically — v3 is different

v4's @theme directive is what makes drop-in token consumption work. For Tailwind v3 (older Catalyst forks or other React projects), import tokens.tailwind.js and spread it into theme.extend in tailwind.config.js. Same tokens, different config mechanism.

Class name collisions with Tailwind defaults

If both tokens.tailwind.css and Tailwind's defaults define a token (e.g. --color-blue-500), Caster's @theme wins because of import order. Caster's namespace prevents most collisions but check token names if behavior is unexpected.

React Server Components and CSS-in-JS

Caster ships pure CSS — no CSS-in-JS dependency. This works fine with React Server Components (CSS-in-JS libraries like styled-components don't). Win.