Customize a component
A feature package gives you a working component. Your design asks for something else. There are four levels of change. Start at the lowest one that does the job: each level up gives you more control, and costs you more updates.
| Level | You change | When Evolve updates |
|---|---|---|
| 1. Style it | classes, CSS, tokens, labels, provider props | You keep everything. |
| 2. Rearrange the parts | which parts render, in what order and wrapper | You keep the parts, not the default arrangement. |
| 3. Take over a part | the markup of one part | You keep the logic, not the markup. |
| 4. Swap a primitive | a ui primitive, such as Button, in every feature at once |
You own that primitive. |
This guide uses one running example: the cart page from
@evolve-storefront/feature-cart, as the
reference B2C storefront customizes it.
Before you start
Section titled “Before you start”-
Your app’s stylesheet must import the package’s
styles.css. Without it, the package’s Tailwind classes are not generated, and nothing tells you so./* src/app/global.css */@import "@evolve-storefront/commerce-ui/styles.css";@import "@evolve-storefront/feature-cart/styles.css";The cart needs
commerce-uitoo, becauseCartSummaryrenders itsPriceSummaryparts. -
Every feature package ships its source under
src/and aCUSTOMIZING.mdnext to itsREADME.md, innode_modules/@evolve-storefront/<package>/. TheCUSTOMIZING.mdlists the package’sdata-componentnames, tokens and seams.
1. Style it
Section titled “1. Style it”One instance: className
Section titled “One instance: className”Every component and every part takes a className. It is merged with
tailwind-merge, so your class replaces a conflicting default instead of fighting it.
// The project makes the column sticky itself, so it turns off the summary's own sticky.<CartSummary cart={cart} checkout={checkoutLink} className="@4xl:static" />Everywhere: data-component CSS
Section titled “Everywhere: data-component CSS”The elements you may style carry a data-component attribute. Target it from your
global CSS to change the component everywhere it renders. These rules come from the
reference B2C storefront:
/* src/app/global.css */[data-component="cart"] { /* Clears the fixed header. */ --cart-summary-top: 6rem;}
/* A wider summary column. The skeleton uses the same element, so it follows. */[data-component="cart-layout"] { @apply @4xl:grid-cols-[minmax(0,1fr)_26rem];}
/* One card per line instead of dividers. */[data-component="cart-line-items"] { @apply gap-4 divide-y-0;}These rules win over the package’s classes because they sit outside a cascade layer,
and Tailwind puts its utilities in @layer utilities.
The cart’s names are listed in
node_modules/@evolve-storefront/feature-cart/CUSTOMIZING.md. The state attributes
you may combine with them (data-disabled, data-current, …) are listed in the
README of @evolve-storefront/ui, under “The state vocabulary you may style against”.
Colors, radius and fonts: tokens
Section titled “Colors, radius and fonts: tokens”The cart has no colors of its own. It reads the shared tokens (--color-foreground,
--color-border, --color-primary, …), so you change those once for the whole
storefront. See Style and brand.
Labels: messages
Section titled “Labels: messages”All cart copy comes from the Cart messages namespace. Override a key in your own
messages to change a label. See Add a language.
Behavior: provider props
Section titled “Behavior: provider props”Behavior is configured on the CartProvider your app already mounts, not by changing
components. For example, keep the “removed. Cancel” row longer, and show a toast after
every add to cart:
import { CartProvider } from "@evolve-storefront/feature-cart/provider";import { toast } from "sonner";
<CartProvider undoTimeout={5000} onLineItemsAdded={({ addedItems }) => toast.success(`${addedItems[0]?.productName} added`)}> {children}</CartProvider>The feature-cart reference lists every
provider prop. For the ways to confirm an add, see
Confirm an add to cart.
What you give up
Section titled “What you give up”Nothing. data-component names are public API, like props: a rename is a breaking
change and is in the changelog.
2. Rearrange the parts
Section titled “2. Rearrange the parts”Cart with no children renders its default composition: a two-column grid with the
lines on the left and the summary on the right.
Some changes still fit the default composition. Pass them to Cart:
<Cart cart={cart} checkout={<Link href="/checkout">Checkout</Link>} renderLink={(line, children) => ( <Link href={`/products/${line.productSlug}`}>{children}</Link> )}/>When you need a different arrangement, pass children. The parts are exported from the
same subpath as Cart. This is a trimmed version of the reference B2C cart page:
import { Cart, CartDiscountCodeForm, CartGrid, CartLineItems, CartSummary, CartSummaryBar,} from "@evolve-storefront/feature-cart/cart";import { useCart } from "@evolve-storefront/feature-cart/hooks";import Link from "next/link";
export function CartPage() { const { data: cart } = useCart(); if (!cart) return null;
const checkout = <Link href="/checkout">Checkout</Link>;
return ( <Cart cart={cart}> <CartGrid> <div className="flex flex-col gap-8"> <CartLineItems cart={cart} /> <Link href="/">Continue shopping</Link> </div> <CartSummary cart={cart} checkout={checkout}> <CartDiscountCodeForm cart={cart} /> </CartSummary> <CartSummaryBar cart={cart} checkout={checkout} /> </CartGrid> </Cart> );}Things to know when you compose:
- Pass
cartto every part. Parts do not read it fromCart. PassvatDisplayto each part too if you show net prices. - With children,
Cartignorescheckout,renderLinkandrenderItem. Set them on the parts instead (CartSummary,CartSummaryBar,CartLineItems). - Keep
CartSummaryBara direct child ofCartGrid. It looks for theCartSummaryin the same grid to know when to show itself. CartSummaryhas slots for project content:checkout,paymentMethods,usps, andchildrenfor the discount code form.- Use the skeletons (
CartSkeleton,CartLineItemsSkeleton, …) andCartEmptyfor the loading and empty states.
What you give up
Section titled “What you give up”The arrangement is yours now. The parts inside it still update. But when a release
changes the default composition, for example to add a part, your page does not get
that change. When you upgrade, check the package’s CHANGELOG.md for changes to
Cart and CartLayout (the default composition).
3. Take over a part
Section titled “3. Take over a part”Sometimes the design moves things around inside a part, and no prop or CSS rule reaches that far. The reference B2C cart row is an example: the quantity sits beside the product instead of under it, and a struck-through original price sits above the amount.
CartLineItems has a seam for this: renderItem replaces the row and leaves the list
in charge of the data and the writes.
-
Copy the shipped row from
node_modules/@evolve-storefront/feature-cart/src/components/parts/cart-line-item.tsxinto your project, for examplesrc/app/[locale]/(main)/cart/_components/cart-line.tsx. Note the package version you copied it from. -
Replace the relative imports with package imports. The types come from the package root (
@evolve-storefront/feature-cart) and from@evolve-storefront/feature-cart/cart. -
Take the writes from the list’s
apiinstead of from props:"use client";import type { CartLineItem } from "@evolve-storefront/feature-cart";import type { CartLineItemApi } from "@evolve-storefront/feature-cart/cart";import { QuantityStepper } from "@evolve-storefront/ui/quantity-stepper";import { useTranslations } from "next-intl";export function CartLine({ line, api }: { line: CartLineItem; api: CartLineItemApi }) {const t = useTranslations("Cart.line");return (<li data-component="cart-line-item" className="grid grid-cols-[auto_1fr_auto_auto] gap-6">{/* image, name, prices: your markup */}<QuantitySteppervalue={line.quantity}onValueChange={api.setQuantity}min={1}disabled={api.isUpdatingQuantity}label={t("quantity-label", { product: line.productName })}decrementLabel={t("quantity-decrease", { product: line.productName })}incrementLabel={t("quantity-increase", { product: line.productName })}/><buttontype="button"data-component="cart-line-item-remove"onClick={api.remove}disabled={api.isRemoving}>{t("remove")}</button></li>);} -
Render it from the list:
<CartLineItems cart={cart} renderItem={(line, api) => <CartLine line={line} api={api} />} /> -
Change the markup to match your design.
Why the api and not your own useRemoveLineItem: api.remove goes through the
list’s undo window, so the list still shows the “removed. Cancel” row. A row with its
own remove hook would start a second undo window that the list cannot see.
Keep the data-component names and the Cart.line message keys. Your global CSS keeps
applying, and your translations keep working.
What you give up
Section titled “What you give up”The markup of that part. Fixes and improvements to the shipped row no longer reach
you: accessible labels, stock messages, price calculations. An earlier release fixed a
row that multiplied the line total by the quantity; a copy made before that fix would
have kept the bug. When you upgrade, compare your copy with the package’s new
cart-line-item.tsx.
You keep everything else: the query, the mutations, the undo window and the messages all stay in the package and keep updating.
4. Swap a primitive
Section titled “4. Swap a primitive”Your project may have its own design system, with its own Button, and want every
feature to use it. Feature packages import the ui primitives by their subpath, such
as @evolve-storefront/ui/button. Point that subpath at your own module, and every
feature renders yours.
This is the last resort. Your bundler resolves the alias once for the whole build, so
it cannot differ per site, and a visual change buys nothing that
tokens and data-component CSS do not. Use it
only when you need different markup everywhere.
-
Write a wrapper that keeps the primitive’s API, and renders your own component. Map the
variantandtonethe features pass to your own:import type { ButtonProps } from "@evolve-storefront/ui/button";import { Button as BrandButton } from "@/components/button";export function Button({ variant = "primary", tone, ...props }: ButtonProps) {return <BrandButton {...props} variant={tone === "destructive" ? "error" : brandVariants[variant]} />;}const brandVariants = { primary: "transactional", secondary: "main", tertiary: "outline" } as const;Export every name the subpath exports, because a feature may import any of them. That includes
buttonProps, which the content blocks call to make a link look like a button: return your own classes from it, with the samedata-component,data-variantanddata-toneattributes. -
Point the subpath at it in
next.config.ts:const nextConfig = {turbopack: {resolveAlias: {"@evolve-storefront/ui/button": "./src/components/ui-button.tsx",},},};
What you give up
Section titled “What you give up”The alias reaches only imports of the subpath. Inside ui, the dropdown menu trigger
and the buttons of a confirm dialog use the shipped buttonProps directly, so they keep
the shipped look. Style those with [data-component="button"] CSS.
You also give up the primitive. Fixes to @evolve-storefront/ui/button no longer
reach any feature, and a new prop a feature starts passing does nothing until your
wrapper handles it. When you upgrade, compare the subpath’s exports and props with
your wrapper. Read Theming the site for when an
alias is the right call.
Next steps
Section titled “Next steps”- Replace the icons in every feature with your own set: Use your own icons.
- Show more data in your row, such as a field the cart query does not fetch: Extend a feature’s data.
- Change colors, radius and fonts for every component at once: Style and brand.

