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
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.
| Prop | Type | Description |
|---|---|---|
label | ReactNode | Already resolved — this package carries no messages. |
value | ReactNode | Already formatted — use |
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.
| Prop | Type | Description |
|---|---|---|
label | ReactNode | Already resolved — this package carries no messages. |
value | ReactNode | Already formatted — use |
Any other props go to the root element, like on a plain HTML element.
PriceSummaryTotalThe grand total.
| Prop | Type | Description |
|---|---|---|
label | ReactNode | Already resolved — this package carries no messages. |
value | ReactNode | Already formatted — use |
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.
| Loading | PriceSummarySkeleton | Placeholder 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>;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>;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>;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)} />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")}/>
