Recipe — Custom theme
Generate a brand palette, wire it into themes/, and sync the email + Stripe mirrors.
You want your fork to stop looking like cartwright's neutral default and look like your brand. This recipe takes you from "pick a primary color" to "every surface reads from your tokens" in about 30 minutes.
Before you start
- A fork of cartwright with
pnpm devrunning. - One source of inspiration: a brand-palette guide, a hero photo, or just a primary hex you already know.
- A contrast checker open (WebAIM or any browser devtool) — palette work without contrast verification ships accessibility bugs.
Steps
- Copy the base theme.
cp themes/generic.css themes/my-shop.css- Edit the token ramp. Open
themes/my-shop.css. Replace the--color-sol-*values with your brand. Keep the variable names — the codebase references them by name everywhere.
@theme {
--color-sol-accent: #d97757; /* primary CTA / price / accent */
--color-sol-sun: #e8b339; /* secondary accent — use sparingly */
--color-sol-cream: #fdf5f1; /* warm offwhite — sidebar / cards */
--color-sol-ink: #1a1a1a; /* headings / body */
--color-sol-muted: #7d6f63; /* secondary text */
}- Swap the import. In
app/globals.css:
@import "tailwindcss";
@import "../themes/my-shop.css"; /* was: ../themes/generic.css */- Sync the email palette. Open
brand.config.ts:emailColors. Mirror the hex values from your theme. Email clients cannot read CSS variables, so this is a manual copy.
emailColors: {
accent: '#d97757',
cream: '#fdf5f1',
sand: '#f5ebde',
ink: '#1a1a1a',
muted: '#7d6f63',
success:'#2d7d4e',
},- Sync Stripe Elements.
brand.config.ts:stripeAppearanceis rendered inside Stripe's iframe — same constraint, manual copy.
stripeAppearance: {
colorPrimary: '#d97757',
colorBackground: '#ffffff',
colorText: '#1a1a1a',
colorDanger: '#dc2626',
borderRadius: '10px',
},- Run the dev server. Inspect every surface. Storefront landing, category, product, cart, checkout, account, admin. Watch for white-on-yellow, dark-on-dark, and any place a hardcoded color slipped past the token migration.
pnpm dev- Run the contrast check. Every CTA, every link, every form input. WCAG AA = 4.5:1 for body text, 3:1 for large text and UI components. The terracotta
#d97757example above passes AA against#1a1a1abut fails against#fdf5f1— adjust before you ship.
What you do NOT need to do
- Edit Tailwind config. The
@theme {}block in your CSS file is the config. - Touch component files. They reference tokens by name.
- Rebuild caches. Turbopack picks up CSS changes in dev. Production builds will pick them up at deploy time.
Optional: Gemini-assisted palette
lib/ai/theme-generator.ts is wired to let admin AI suggest palettes from a reference image. The current implementation accepts an image and returns palette suggestions — not pixel-perfect extraction. Treat the output as a starting point, not the final palette.
Skip the AI step on your first theme. Most brands have an existing primary color and accent; manual is faster than prompting. The AI path is most useful when you have a hero photo but no brand guidelines yet.
Verification
- Cartwright admin
/adminlooks branded (header, buttons, links). - Storefront landing reads correctly in dark mode (Fumadocs ships a toggle; cartwright forks usually do not — verify your fork's stance).
- Email previews in
.mail-previews/render with your new colors (trigger a magic link to test). - Stripe Elements on
/checkoutmatch the rest of the page (palette + border-radius).