Wedding Color Studio landing page showing palette builder with Soft Garden Bloom palette

Wedding Color Studio — A Case Study in Shipping AI as Product

Wedding Color Studio is a David’s Bridal-branded React + Express + Gemini application I built that turns a five-color palette into a named identity, a generative editorial mood board, a six-scene “vision of your day,” and a shoppable bridesmaid lineup — wired end-to-end with the polish of a shipping product, not a demo.

Try it live → colors.davidsbridal.com

Wedding Color Studio landing page showing palette builder with Soft Garden Bloom palette
The Color Studio landing experience — palette builder, AI mood board generation, and curated browsing

What the app actually does

Calling it a “wedding palette builder” undersells it. The user picks up to five colors from a curated set of 64 swatches (or supplies their own hex), and the app then does several things that are each a feature in their own right.

It names the palette using Gemini under a strict 3-word formula (“Midnight Desert Rose”, “Modern Coastal Blues”) that I enforce with a heavily example-driven prompt — 18 positive exemplars, 7 explicit bad outputs, and rules about when a color family word may appear in each slot. The model’s natural failure mode is stacking literal swatch names, so the anti-examples are doing real work.

It generates a 16:9 editorial flat-lay mood board via Gemini’s image generation, using a prompt engine that picks a tactile material per swatch — velvet for “Chocolate”, silk dupioni for “Champagne”, pressed dried flower for “Petal” — and forbids the model from rendering swatch chips inside the photo. A deterministic function (inferSwatchMaterial) reads swatch names with regexes and falls back to a luminance heuristic for unmatched names. The model still renders the material; the function just steers it. Use code for what code is good at, prompts for the rest.

AI-generated editorial mood board for Terra Novella palette with Terracotta, Latte, Pistachio, Ivory, and Champagne
The Terra Novella mood board — each material is deterministically selected based on swatch names, then rendered by Gemini

It builds a six-scene Vision Board — ceremony, tablescape, bouquet, cake, invitation, wedding party — each with its own scene template that references the style of real wedding photographers and explicitly bullet-points the things to avoid (impossibly perfect symmetry, identical pastel-slip bridesmaids, AI hands).

Vision Board generating six AI-imagined wedding scenes from the Terra Novella palette
The Vision Board — six AI-imagined scenes generated in parallel, each with its own scene-specific prompt template

It recolors a reference bridesmaid photo through Gemini’s image-to-image mode using the first three palette colors as a constraint, producing a hero CTA that’s shared between the mood board and vision board modals — so opening the second modal doesn’t regenerate the same hero.

And it browses David’s Bridal bridesmaid inventory via Shopify’s GraphQL Admin API, with each swatch mapped through aliases (Quartz → “Dusty Rose”, Cinnamon → “Rust”, Sapphire → “Navy Blue”) so the palette translates into a filtered product feed that actually returns results.

The dynamic CTA: from vision to shopping cart

The moment a couple finishes generating their vision board, the app doesn’t just leave them admiring AI images. A full-bleed CTA uses the AI-generated wedding party scene — their specific colors, rendered on real-looking people in varied silhouettes — as a background image, with a “Shop Now” button that opens David’s Bridal pre-filtered to bridesmaid dresses matching their palette. The headline adapts: “Love your vision? Start with the dresses.” It’s the conversion moment the entire experience builds toward, and it works because the background image already shows the user their palette on actual dresses, making the shopping intent feel like a natural next step rather than a sales pitch.

Dynamic CTA using palette colors to prompt shopping for bridesmaid dresses at David's Bridal
The dynamic CTA — the AI-generated wedding party scene becomes a shoppable bridge to David’s Bridal inventory

Every successful generation persists to a public Pinterest-style inspiration feed through a Google Cloud Storage manifest, with a custom interleaving algorithm that prevents the same palette appearing twice in a row. The feed currently has 120+ boards from real user sessions.

Get Inspired feed showing real wedding inspiration matched to selected colors
The Get Inspired feed — real user-generated mood boards surfaced through a custom interleaving algorithm

The prompt engineering is production-grade

This isn’t vibes prompting. The mood-board prompt is a ~1,500-word specification that reads like a brief to a working photographer. It iteratively absorbed observed failure modes:

“ABSOLUTELY CRITICAL — DO NOT RENDER A SWATCH STRIP, COLOR CHIPS, PAINT SAMPLES…” is there because the model kept inventing mislabeled chips that contradicted the real palette already shown in the UI. “Do NOT default to burlap, jute, mesh weave…” exists because earlier renders all looked like the same shoot. The wedding-party scene explicitly counter-prompts the most common AI tell — everyone wearing identical pastel slips in slightly different colors — and asks for varied silhouettes, a believable mix of skin tones, ages, and body types.

The palette-naming prompt is even more disciplined: a formal VIBE + SETTING + COLOR/MOOD formula, 18 positive exemplars, 7 explicit bad outputs (“Ballet Berry Amethyst”, “Coastal Serenity”, “Whimsical Garden Fairytale Romance” ← four words), and a one-shot retry on transient 503/429 errors.

Four AI surfaces, not one

The backend exposes four distinct AI endpoints, each with its own contract: text generation with formula-enforced output for palette naming, image generation with aspect-ratio control for mood boards, parameterized scene generation across nine templates for the vision board, and image-to-image recoloring for the bridesmaid CTA. The client supports both API key and Vertex AI auth modes, meaning it can ship behind an enterprise Google Cloud account without code changes.

React engineering that respects StrictMode

A lot of AI-app demos break the moment you turn on React StrictMode. This one is built for it. Generation-ID refs discard stale responses when a user re-triggers mid-flight. AbortController per request with cleanup in effect teardown. A deferred-kickoff pattern neutralizes StrictMode’s intentional double-invoke without losing the real call.

The usePaletteNaming hook exposes an idempotent, in-flight-dedupe promise so both modals can await the same naming request without duplicating it. The readBootState() lazy initializer hydrates from URL hash or localStorage in the same tick the component first mounts, so the persistence effect never sees an empty palette flicker that would clear the URL hash.

Color theory, modeled correctly

The recommendation engine implements real HSL color math with five ranked strategies: palette completion against the 80-palette curated library, complementary (180° opposite), analogous (±30°), popular pairings from co-occurrence data, and add-a-neutral if none is present. It’s unit-tested with Vitest, and the test contract that matters is enforced: no recommendation will ever echo back a swatch the user already selected.

The share pipeline is its own product

A canvas renderer takes the generated image, the palette, the palette name, and a hex-codes toggle, and composes a branded share asset at the exact spec of five platforms — Instagram 1080×1080, Feed Post 1080×1350, Story/Reel 1080×1920, Pinterest 1000×1500, and Facebook/X 1200×675. Each format is selectable via tabs, and the preview updates live as the user switches between them. It picks black or white text per swatch via luminance contrast, supports navigator.share() on mobile with clipboard and download fallbacks, and exposes a Pin It button that opens Pinterest’s create endpoint with the palette URL and description prefilled.

The share drawer is nested inside the modal without propagating Escape to the outer dialog. Six actions line up below the preview: Share, Download, Copy Image, Pin It, Copy Link, and Copy Hex — each one engineered for the specific platform’s sharing constraints. The same share pipeline works on both mood board and vision board scenes, meaning a couple can share their ceremony rendering to Instagram Stories and their flat-lay to Pinterest with the correct aspect ratio for each.

Share modal showing platform-specific format tabs and share actions for the Terra Novella mood board
The share pipeline — platform-specific asset rendering with format tabs, luminance-aware text contrast, and six distinct share actions

The URL itself is shareable — palette colors and name are encoded into the hash so when a couple shares the link, the preview card reflects their palette via synced og:title and twitter:title meta tags.

A taste-driven data layer

The 80+ curated palettes aren’t placeholder data. They’re researched and opinionated — there’s a section called “2026 Forecast: The Chromatic Narrative” with entries like “Olive & Plum” (the +1,380% trending pairing of the season), “Sage Wave” encoding a designer 60-30-10 color formula, and “Long Live Red” explicitly framed as a double-tradition wedding palette. This is wedding-industry domain knowledge expressed as code, with style, season, trending, and description fields that feed into the AI prompt as styling direction.

Curated wedding color palettes in the Color Studio browse experience
Curated palettes — 80+ opinionated combinations with season, style, and trend data feeding the AI

Backend hardening

This isn’t a localhost demo. It’s configured like it lives behind Cloud Run: Helmet with documented CSP trade-offs, CORS allowlist, per-route rate limiters (20/min for generation, 30/min for bridesmaid queries), 64kb body limits, strict request validation (hex regex, length caps, control-char stripping, scene allow-lists), and trust-proxy configuration for correct client-IP attribution behind a load balancer.

The GCS inspiration store uses a private bucket with uniform bucket-level access. The Express server proxies image bytes with cache-control and ETag passthrough for cheap 304 responses after the 24-hour max-age expires. Manifest writes use a tolerant read-modify-write that explicitly accepts the race condition at current volume — and documents that decision in the code.

Multi-stage Dockerfile builds the Vite frontend in node:20-alpine, copies /dist into a backend-only runtime stage, and serves /api/* plus the SPA from the same origin on port 8080. GitHub Actions CI runs lint + test + build on both frontend and backend for every push.

Accessibility wasn’t skipped

Both modals implement role="dialog" with aria-modal="true", Escape to close, real focus traps (Tab and Shift-Tab wrap inside the modal), focus-on-open, and focus-restore-on-close. The nested share drawer doesn’t propagate Escape to the outer modal. The browse-by tab bar implements proper roving tabindex with ArrowLeft/ArrowRight navigation. None of this is exotic — all of it is rarely done in side-project AI apps.

The one-sentence version

This is what it looks like to take a generative-AI capability and ship it as a product — engineering the prompt, the model integration, the React state machine, the share pipeline, the backend hardening, the storage layer, the domain data, and the UX funnel as one coherent system, with comments that read like a postmortem of every decision that got reworked along the way.

Leave a Reply

I'll probably write more things and you might just want to read those too.

Subscribe to make sure you don't miss any of the good stuff.

Discover more from MIKE BAL

Subscribe now to keep reading and get access to the full archive.

Continue reading