UnitPrice
A 330ml bottle priced at €4.20 also shows "€12.73 / 1l" on the shelf. A 500g bag of coffee shows "€19.98 / kg". These per-unit labels are how customers compare prices across pack sizes, and in many EU markets they are required by law on grocery and household goods. The shape connectors return for them is UnitPrice: a Money value, the quantity the variant actually contains, and the reference quantity the price is normalized to.
import type { UnitPrice } from '@laioutr-core/core-types/common';
interface UnitPrice {
price: Money; // cost for the reference quantity
quantity: Measurement; // the variant's actual content (e.g. 330ml)
reference: Measurement; // the unit shown to the customer (e.g. 100ml or 1l)
}
UnitPrice is a flat record, not a discriminated union. Variants would not pay their way: the rendering convention ("price slash unit") is the same whether the product is a drink, a detergent, or a bag of flour.
Fields
price
A Money value carrying the cost for the reference quantity, not the variant's actual content. A 330ml bottle that retails at €4.20 with a per-100ml unit price has price: { amount: 127, currency: 'EUR' } (€1.27 per 100ml), regardless of how many ml the variant contains.
The currency must match the rest of the entity's pricing. Mixing a USD unitPrice.price with a EUR prices.price is structurally possible but invalid; the storefront has one currency per market.
quantity
A Measurement ({ value: number, unit: string }) describing how much of the product the variant actually contains. For a 330ml bottle of soda, quantity: { value: 330, unit: 'ml' }. For a 500g bag of coffee, { value: 500, unit: 'g' }. This is informational; the renderer does not divide by it.
reference
The unit the price is normalized to. This is the value shown after the slash on the shelf label, the one customers compare across products. Conventions vary by category:
| Product category | Common reference | Example label |
|---|---|---|
| Beverages | 100ml or 1l | €1.27 / 100ml |
| Solid food | 100g or 1kg | €19.98 / kg |
| Detergents | 1l or 1kg | €2.45 / l |
| Paper goods | 1sheet or 100sheet | €0.04 / sheet |
| Wine, spirits | 750ml (the bottle), 1l | €18.66 / 750ml |
Stick to the reference that customers expect for the category. The unit field accepts the ECMA-402 sanctioned identifiers plus the Laioutr-known units ('ml', 'l', 'g', 'kg', 'sheet', 'item', etc.). Custom strings work but may not localize correctly.
Formatting with $unitPrice
A UnitPrice value is just data. Turning it into the localized "price slash unit" label is the job of $unitPrice, a global helper the UI Kit registers as a Nuxt plugin and that is auto-available in templates and setup():
<template>
<span>
{{ $unitPrice({
price: { amount: 1399, currency: 'EUR' },
quantity: { value: 330, unit: 'ml' },
reference: { value: 100, unit: 'ml' },
}) }}
</span>
<!-- Renders '13,99 € / 100 ml' in de-DE, '€13.99 / 100 ml' in en-IE. -->
</template>
Outside templates, access it through the Nuxt app:
const { $unitPrice } = useNuxtApp();
const label = $unitPrice(variant.prices.unitPrice);
The signature is:
$unitPrice(unitPrice: UnitPrice): string
$unitPrice composes the global $money and $measurement formatters with their defaults; the result is exactly `${$money(unitPrice.price)} / ${$measurement(unitPrice.reference)}`. Both inner formatters read the active locale, so the label localizes alongside the rest of the page.
The quantity field is never rendered: only price and reference reach the output. To show the pack size alongside the unit price (330 ml · 1,27 € / 100 ml), render it yourself with a separate $measurement(unitPrice.quantity) call.
$unitPrice is resilient to falsy input: a missing unitPrice renders as the empty string instead of throwing. This keeps partial SSR responses from crashing the page, but a blank label where one was expected usually means the connector returned no unitPrice for the variant.The UI Kit's VariantSelectionCard renders this label automatically when its unitPrice prop is set.
For connector authors
When the source backend has a unit price (Shopify exposes it on ProductVariant.unitPrice and ProductVariant.unitPriceMeasurement; Adobe Commerce surfaces it on attribute-driven products; Hygraph and Storyblok let editors enter it manually), map the three fields directly:
import type { UnitPrice } from '@laioutr-core/core-types/common';
import { Money } from '@screeny05/ts-money';
const toUnitPrice = (raw: ShopifyUnitPrice): UnitPrice => ({
price: Money.fromDecimal(raw.amount, raw.currencyCode),
quantity: { value: raw.measuredVariant.value, unit: raw.measuredVariant.unit },
reference: { value: raw.measuredReference.value, unit: raw.measuredReference.unit },
});
If the backend gives you only the variant's pack size and a per-100g convention, compute the price yourself rather than dropping the field entirely. Use @screeny05/ts-money for the math so the minor-unit rounding and zero-decimal currencies are handled for you, instead of computing amount by hand:
import { Money } from '@screeny05/ts-money';
const variantWeightG = 500; // 500g pack
const referenceG = 100; // per 100g
const variantPrice = Money.fromDecimal(14.99, 'EUR'); // { amount: 1499, currency: 'EUR' }
const unitPrice: UnitPrice = {
price: variantPrice.multiply(referenceG / variantWeightG), // { amount: 300, currency: 'EUR' }
quantity: { value: variantWeightG, unit: 'g' },
reference: { value: referenceG, unit: 'g' },
};
// price.amount === 300, renders as '3,00 € / 100 g'
multiply rounds the result to the currency's smallest unit, so 14.99 € × (100 / 500) lands on 3.00 € rather than a fractional-cent amount. A ts-money instance is structurally compatible with the Money interface, so you can assign it straight to unitPrice.price.
When the backend has no unit-price data at all, leave the field undefined. The storefront skips the label cleanly; do not fabricate one from the headline price alone, since that produces a label customers cannot compare against competitor products in the same category.
Related
Money: the type behindUnitPrice.price, including the$moneyhelper and zero-decimal currency handling.Measurement: the type behindUnitPrice.quantityandUnitPrice.reference, including the$measurementhelper and the supported unit codes.- Locale-aware formatting: the full set of UI Kit formatters (
$money,$measurement,$unitPrice,$timespan,$duration). - Product and ProductVariant: the entities whose
pricescomponents carryUnitPricevalues. - schema.org/UnitPriceSpecification and the Shopify ProductVariant.unitPrice docs: the upstream conventions Laioutr follows.
Money
The flat record returned anywhere a connector or component schema yields a price. Two fields, locale-aware rendering through the $money formatter.
Measurement
The flat record carrying a numeric value and a unit identifier. Locale-aware rendering through the $measurement formatter, with metric/imperial selection driven by the active language.