Skip to content

PriceSummary

The one amount-breakdown rendering: a cart summary, an order breakdown, a quote total, a checkout recap. Every one of those is the same list of label/amount pairs with an emphasised total, and before this each feature had its own copy — which is how they ended up disagreeing on the total's weight and on whether the figures were tabular.

import { PriceSummary } from "@evolve-storefront/commerce-ui/price-summary";

Renders inside AddressConfigProvider.

Design notes

Compound, and it owns no i18n: the caller passes resolved strings and already formatted money. PriceSummary frames a panel; a surface that wants the rows without a box (an order detail column, say) uses PriceSummaryList alone.

Preview

DefaultOpen in Storybook ↗

Usage

The cart's use: a framed panel with the rows and a call to action.

// Labels arrive resolved and amounts arrive formatted — this package has no i18n.
<PriceSummary>
<PriceSummaryList>
<PriceSummaryRow label={t("subtotal")} value={formatMoney(subTotal, locale)} />
<PriceSummaryRow label={t("shipping")} value={t("shipping-free")} />
<PriceSummaryTotal label={t("total")} value={formatMoney(total, locale)} />
</PriceSummaryList>
<CheckoutButton />
</PriceSummary>;

Props

No props of its own.

Change the layout

Pass children to PriceSummary to arrange its parts yourself. Every part is exported from @evolve-storefront/commerce-ui/price-summary.

// Labels arrive resolved and amounts arrive formatted — this package has no i18n.
<PriceSummary>
<PriceSummaryList>
<PriceSummaryRow label={t("subtotal")} value={formatMoney(subTotal, locale)} />
<PriceSummaryRow label={t("shipping")} value={t("shipping-free")} />
<PriceSummaryTotal label={t("total")} value={formatMoney(total, locale)} />
</PriceSummaryList>
<CheckoutButton />
</PriceSummary>;
PriceSummaryListThe rows themselves, as a description list.

A dl is what this data is, and it is why the rows are components rather than markup a caller writes: dl may only contain dt/dd groups, so hand-written div/span rows — which is what both features had — are invalid inside one.

No props of its own.

PriceSummaryRowOne label/amount pair.
PropTypeDescription
labelReactNode

Already resolved — this package carries no messages.

valueReactNode

Already formatted — use formatMoney from @evolve-storefront/core/money.

note?ReactNode

A parenthesised aside after the label, e.g. a reverse-charge note.

Any other props go to the root element, like on a plain HTML element.

PriceSummarySubtotalA row that sums the rows above it — the net line under a build-up of items, discount and delivery, with the grand total still to come below.

A role rather than an attribute a caller invents, because a summary that lists a build-up needs the rule over this row and nothing else on the page does: everywhere else the first row opens the list and the panel's own edge is already above it. Without a role for it, a consumer reaches for :first-child or invents its own attribute in app CSS — which is a hook the package cannot see and cannot keep.

It is not the total, and that is why it is not a flag on PriceSummaryTotal: "adds up what is above" and "is the amount you pay" are different claims, and a receipt can carry both.

PropTypeDescription
labelReactNode

Already resolved — this package carries no messages.

valueReactNode

Already formatted — use formatMoney from @evolve-storefront/core/money.

Any other props go to the root element, like on a plain HTML element.

PriceSummaryTotalThe grand total.
PropTypeDescription
labelReactNode

Already resolved — this package carries no messages.

valueReactNode

Already formatted — use formatMoney from @evolve-storefront/core/money.

Any other props go to the root element, like on a plain HTML element.

Loading and empty states

Render these while the data loads or when there is nothing to show, so the page does not jump when it arrives.

LoadingPriceSummarySkeletonPlaceholder rows at the real row's height and rhythm.

Examples

UnframedThe order's use: the same rows without a panel, ruled above the total and with a note on the reverse-charge line.
// No panel: the list alone, ruled off with `className`.
<PriceSummaryList className="border-border border-t pt-3">
<PriceSummaryRow label={t("subtotal")} value={formatMoney(subtotal, locale)} />
<PriceSummaryRow
label={t("reverse-charge")}
value={formatMoney(zero, locale)}
note={t("reverse-charge-note")}
/>
<PriceSummaryTotal
label={t("total")}
value={formatMoney(total.gross, locale)}
className="mt-1 border-border border-t pt-2"
/>
</PriceSummaryList>;
Open in Storybook ↗
LoadingPlaceholder and panel adjacent — the rows must land at the same height.
// Frame the skeleton the same way you frame the list, and the panel cannot
// drift from its own placeholder.
<PriceSummary>
<PriceSummarySkeleton rows={3} />
</PriceSummary>;
Open in Storybook ↗
Every Row RoleEvery row role, and what each one is for. Four components, and choosing between them is the whole API — the emphasis and the rules are theirs, not a caller's to restate.
<PriceSummaryList>
<PriceSummaryRow label={t("items")} value={formatMoney(items, locale)} />
<PriceSummaryRow
label={t("discount", { code })}
value={formatMoney(discount, locale)}
/>
<PriceSummaryRow label={t("shipping")} value={formatMoney(shipping, locale)} />
<PriceSummarySubtotal label={t("subtotal")} value={formatMoney(net, locale)} />
<PriceSummaryRow
label={t("vat", { rate: 21 })}
value={formatMoney(vat, locale)}
note={t("vat-note")}
/>
<PriceSummaryTotal label={t("total")} value={formatMoney(gross, locale)} />
</PriceSummaryList>;
Open in Storybook ↗
Net And GrossThe same amounts, net and gross. The two presentations a B2B storefront switches between with vatDisplay, side by side — which is the only way to see that they reconcile to the same figure.
// Net: tax as an added line over a subtotal that excludes it.
<PriceSummarySubtotal label={t("subtotal-excl")} value={formatMoney(net, locale)} />
<PriceSummaryRow label={t("vat", { rate: 21 })} value={formatMoney(vat, locale)} />
<PriceSummaryTotal label={t("total")} value={formatMoney(gross, locale)} />
// Gross: tax already in the prices, stated as a memo under the total.
<PriceSummaryTotal label={t("total-incl")} value={formatMoney(gross, locale)} />
<PriceSummaryRow label={t("vat-included", { rate: 21 })} value={formatMoney(vat, locale)} />
Open in Storybook ↗
Reverse Charged VatA zero that has to be said out loud. Reverse-charged VAT is €0.00 because the liability shifted to the buyer, and a row omitted for being zero reads as tax having been forgotten — on the one invoice where a buyer's accountant is looking for it.
// Zero, and stated — `vatShifting` present means the liability moved.
<PriceSummaryRow
label={t("vat-reverse-charged")}
value={formatMoney(zero, locale)}
note={t("reverse-charge-note")}
/>
Open in Storybook ↗