HextaUI

Prices for a storefront in any currency and locale. Handles sales, ranges, unit prices, free items and prices that change with the variant.

Open in new tab

Price is the one component every storefront block uses to show money. It takes a Money value, { amount: "48.00", currencyCode: "USD" }, and formats it with Intl for the shopper’s locale: $48.00 in the US, 48,00 € in Germany, ¥7,200 in Japan with no decimals, ₹4,000 with Indian grouping.

A compare-at price higher than the price shows struck through with the saving next to it, as a percentage or an amount. Screen readers hear “Sale price $36, Regular price $48” instead of two bare numbers. A range reads “From $28” on a product card or “$28 – $78” in full, and collapses to one price when both ends match. Zero reads Free, and a missing price reads Price unavailable or your own wording.

When the price changes, for example when a shopper picks a bigger size, only the digits that changed roll to their new value, with tabular figures so nothing around the price moves. Unit prices sit on their own line under the price.

The money helpers behind it, money, addMoney, multiplyMoney, sumMoney, fromMinorUnits and discountPercent, work in integer minor units so 0.1 + 0.2 is 0.30, and they refuse to add two currencies together. Cart, checkout and dashboard blocks use the same helpers.

  1. Obtén HextaUI Pro

    Un solo pago desbloquea todos los bloques, incluidos los futuros. Ver precios.

  2. Añade el registro Pro a components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  3. Añade tu token

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  4. Añade el bloque

    pnpm dlx shadcn@latest add @hextaui-pro/price

From your product data

Pass the price and compare-at price as Money, the same { amount, currencyCode } shape Shopify’s Storefront API returns. A compare-at price that isn’t higher is ignored, so a data mistake never shows a fake sale.

import { Price } from "@/components/blocks/price/price"
import type { Money } from "@/components/blocks/price/money"

type ProductPriceProps = {
  price: Money
  compareAtPrice: Money | null
}

export function ProductPrice({ price, compareAtPrice }: ProductPriceProps) {
  return (
    <Price
      size="lg"
      value={price}
      compareAt={compareAtPrice}
      locale="en-US"
      trailingZeros={false}
    />
  )
}

From minor units

Stripe, Paddle and most payment APIs send integers in the currency’s smallest unit. fromMinorUnits turns 4800 USD into $48.00 and 4800 JPY into ¥4,800, because yen has no cents.

import { fromMinorUnits } from "@/components/blocks/price/money"
import { Price } from "@/components/blocks/price/price"

type StripePrice = {
  unit_amount: number
  currency: string
}

export function PlanPrice({ price }: { price: StripePrice }) {
  return <Price value={fromMinorUnits(price.unit_amount, price.currency)} />
}

Unit prices

Show the price per 100 mL or per kg under the price, as EU rules require for goods sold by volume or weight. The money helpers do the math in minor units, so totals never drift.

import { fromMinorUnits, toMinorUnits, type Money } from "@/components/blocks/price/money"
import { Price } from "@/components/blocks/price/price"

export function RefillPrice({ price, milliliters }: { price: Money; milliliters: number }) {
  const per100 = fromMinorUnits(
    (toMinorUnits(price) * 100) / milliliters,
    price.currencyCode
  )
  return (
    <Price
      value={price}
      unitPrice={{ amount: per100, per: "100 mL" }}
      locale="de-DE"
    />
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
PriceThe current price, compare-at price, saving and unit price.
PriceSkeletonA loading placeholder the size of a price.
money, fromMinorUnits, toMinorUnitsBuild Money from major units, minor units or back.
addMoney, subtractMoney, multiplyMoney, sumMoney, compareMoneyExact money math in minor units. Throws when currencies differ.
formatMoney, discountPercentFormat Money as text, and work out the saving as a whole percent.

Precio

Also takes span props.

PropTipoPredeterminado
valueThe price, or a range. null shows the unavailable label.
Money | { min: Money; max: Money } | null–
compareAtThe original price. Shown only when higher than value.
Money | null–
unitPriceA unit price line, such as $4.17 / 100 mL.
{ amount: Money; per: string }–
savingHow to show the saving on a sale.
"percent" | "amount" | "none""percent"
rangeShow a range as “From $28” or “$28 – $78”.
"from" | "span""from"
sizeText size. The compare-at price and saving scale with it.
"sm" | "default" | "lg" | "xl""default"
localeLocale for formatting. Fixed by default so server and browser render the same text.
Intl.LocalesArgument"en-US"
currencyDisplayHow to write the currency.
"symbol" | "narrowSymbol" | "code" | "name""narrowSymbol"
trailingZerosSet false to show $48 instead of $48.00. Prices with cents keep them.
booleantrue
animatedRoll changed digits when the price changes.
booleantrue
labelsWords for sale, regular, from, free, unavailable, save and per.
Partial<PriceLabels>–
PropTipoPredeterminado
amountA decimal string in major units, like "48.00".
string–
currencyCodeAn ISO 4217 code, like "USD".
string–
  • Sale prices read with their meaning, “Sale price $36, Regular price $48, Save 25%”, and a range reads “$28 to $78”.
  • Rolling digits are hidden from screen readers, which get the formatted price as plain text. With reduced motion, the price changes without rolling.
  • Figures are tabular, so a changing price never shifts the layout around it.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Price. Cada uno se instala por separado.

Código

4 archivos, añadidos a components/blocks/price.