Skip to content

Set Up an Evolve Storefront App

Wire a new client storefront (a Next.js app) as a thin consumer of the @evolve-storefront frontend packages: logic and primitives stay npm dependencies, the app owns only composition, brand, and page markup.

  • Bootstrapping a new client storefront on top of @evolve-storefront.
  • Adding an @evolve-storefront package (ui, layout, feature-*) to an existing app and wiring it correctly.
  • Diagnosing a component that renders subtly unstyled / broken — almost always a missing per-package styles.css import (see The one that bites).
  • Setting up locale message merging or a feature provider.

Read ../../evolve-storefront/AGENTS.md alongside this — it is the authoritative consumer contract. This skill is the step-by-step; that file is the why.

The layered model (what the app is responsible for)

Section titled “The layered model (what the app is responsible for)”
Layer What Lives where
1 Headless logic @evolve-storefront/feature-* (npm)
2 Presentational primitives @evolve-storefront/ui (npm)
3 Composition / page markup + brand your app

The app is Layer 3 and must stay thin. Any business rule belongs in a Layer-1 hook — if it could have a bug, it does not belong in composition markup. ui is domain-agnostic and prop-driven; never add data fetching there.

  • Node.js >= 24.8 (.nvmrc), pnpm >= 11 via Corepack — run corepack enable.
  • A Next.js app (App Router) with Tailwind CSS v4 (CSS-first — no tailwind.config.*). If starting fresh, pnpm create next-app then migrate styling to Tailwind v4.
  • Access to the @evolve-storefront/* packages (workspace, or the private npm registry the org publishes to).

Depend only on what you use. Typical starting set:

// package.json (versions: match the registry; workspace:* inside evolve-platform)
{
"dependencies": {
"@evolve-storefront/ui": "^0.2.0",
"@evolve-storefront/layout": "^0.2.0",
"@evolve-storefront/feature-shopping-list": "^0.2.0",
"next-intl": "^3",
"react": "^19",
"react-dom": "^19",
"tailwindcss": "^4"
}
}

Feature packages pull their own deps (@evolve-storefront/core, errors, ui) transitively — you do not list those unless you import them directly.

2. Wire the brand + every package’s styles.css (the critical step)

Section titled “2. Wire the brand + every package’s styles.css (the critical step)”

Tailwind v4 only generates the classes it can find in scanned sources. Each Evolve package ships a tiny styles.css that registers its own source dir with a package-local @source, exposed at the ./styles.css export. Your app’s global stylesheet must @import one for every package it consumes:

/* app/globals.css (or src/app/globals.css) */
/* 1. Your brand: the Tailwind entry (@import "tailwindcss"), your @theme tokens,
and the base layer. Start from evolve-storefront packages/ui/global.css
and make it your own — it is the DEFAULT brand, not a package export. */
@import "./brand.css";
/* 2. One line per consumed package — this is what makes their classes exist. */
@import "@evolve-storefront/ui/styles.css";
@import "@evolve-storefront/layout/styles.css";
@import "@evolve-storefront/feature-shopping-list/styles.css";

Use CSS @import (one Tailwind compilation unit), not @source "@evolve-storefront/<pkg>" — the latter works but hardcodes the package’s internal layout into your app and breaks when the package moves its sources.

Brand contract. Your brand @theme must define the color/radius/font tokens and the page-shell tokens --container-max, --container-padding, --container-padding-lg — ui’s Container reads them, so content width is a token change, not a component change.

Text is semantic HTML. There is no Text/Display component. Style <p>/<small> and the h1–h6 rules in your brand’s @layer base; use Heading only to map a level to a tag (it emits no classes).

The one that bites: per-package styles.css

Section titled “The one that bites: per-package styles.css”

Forgetting a package’s @import fails silently — no error, no warning. The component renders with its structure but none of its utility classes, so it looks like a bug in the component rather than a missing stylesheet. It recurs every time you adopt another package, because the app already worked before.

If an @evolve-storefront component looks subtly wrong, check globals.css for its styles.css import first, before touching the component.

Every package that ships strings ships all five locales — en (US), nl, de, fr, es — under its ./messages/* export (e.g. @evolve-storefront/feature-shopping-list/messages/en.json). en is the source and the only guaranteed-complete locale. Merge each consumed package’s file for the active locale in your next-intl request config, then let app messages win last:

// i18n/request.ts (sketch)
import deepmerge from "deepmerge";
async function load(locale: string) {
const [ui, layout, shoppingList, app] = await Promise.all([
import(`@evolve-storefront/layout/messages/${locale}.json`),
import(`@evolve-storefront/feature-shopping-list/messages/${locale}.json`),
import(`./messages/${locale}.json`),
]);
// app messages last so a brand can override any package string.
return deepmerge.all([layout.default, shoppingList.default, app.default]);
}

Status keys mirror the evolve-framework GraphQL enums verbatim (e.g. CANCELLED, Cancelled) — never normalize a key’s casing/spelling. Register is informal by default (je/du/tu/tú); a formal brand (German B2B → Sie) overrides the affected keys in the app’s own message files.

4. Wire a feature (provider + composition)

Section titled “4. Wire a feature (provider + composition)”

Layer-1 features expose a ./provider, headless hooks (useShoppingList…), and prebuilt islands. Mount the provider once, compose the islands in your page:

// Layer 3 — composition only. No business rules here.
import { ShoppingListProvider } from "@evolve-storefront/feature-shopping-list/provider";
import { ShoppingListOverview } from "@evolve-storefront/feature-shopping-list/shopping-list-overview";
export default function Page() {
return (
<ShoppingListProvider>
<ShoppingListOverview headingLevel={1} />
</ShoppingListProvider>
);
}

Pass a headingLevel to any view that renders headings (page 1 / section 2 / card 3) — a feature can’t know its own document depth.

Gated (auth-dependent) queries stay pending, never settle empty. When branching on a gated hook, treat only isSuccess && !data as a real miss — isLoading/bare data will 404 on a hard load while soft navigation works.

5. Overlays & icons (don’t reach for another library)

Section titled “5. Overlays & icons (don’t reach for another library)”
  • One overlay library: react-aria-components. Adding a second (e.g. Radix portalled popovers) is a correctness bug, not a style choice — a foreign portal renders as a sibling of an RAC dialog and its options become unclickable inside a modal.
  • Icons go through the map. Render <Icon name="…" /> from @evolve-storefront/ui/icon (names describe the role, not the glyph). Never import lucide-react in the app.
corepack enable
pnpm install
pnpm tsc # or: pnpm check
pnpm dev # then click through: are the Evolve components fully styled?

Styling regressions don’t fail tsc or tests — look at the rendered app. The fastest check that every package is wired is a page that mounts one component from each consumed package.

  • Node 24.8 + pnpm via corepack; Tailwind v4 (no tailwind.config.*).
  • globals.css: brand entry first, then one @import ".../styles.css" per consumed package.
  • Brand @theme defines color/radius/font and --container-* tokens.
  • next-intl merges each package’s messages/<locale>.json, app last; all five locales present; status keys left verbatim.
  • Feature providers mounted; composition is thin (no business rules).
  • Overlays are RAC only; icons via @evolve-storefront/ui/icon.
  • Rendered app visually verified — not just tsc.
  • evolve-storefront/AGENTS.md — authoritative consumer contract (styling, layers, locales, icons, overlays).
  • evolve-storefront/packages/ui/global.css — the default brand to fork.
  • evolve-storefront/README.md — the copy-pasteable app stylesheet listing every package.