Common Types

UnitPrice

The price-per-unit type that pairs a Money value with a quantity and a reference measurement, e.g. "€13.99 / 100g".

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 categoryCommon referenceExample label
Beverages100ml or 1l€1.27 / 100ml
Solid food100g or 1kg€19.98 / kg
Detergents1l or 1kg€2.45 / l
Paper goods1sheet or 100sheet€0.04 / sheet
Wine, spirits750ml (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():

components/UnitPriceLabel.vue
<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.

Copyright © 2026 Laioutr GmbH