---
name: price
title: Price
url: https://ui.frontic.com/components/ui/price.md
tier: ui
install: npx @frontic/ui add price
files:
  - components/ui/price/Price.vue
  - components/ui/price/PriceAmount.vue
  - components/ui/price/PriceDiscount.vue
  - components/ui/price/PriceHint.vue
  - components/ui/price/PriceRef.vue
  - components/ui/price/index.ts
registryDependencies:
  - badge
  - format
dependencies:
  - class-variance-authority
  - reka-ui
---

# Price

Shows a price in the shopper's locale, with the original price and the percentage saved when it is reduced, and a hint such as tax and shipping.

## Install

```sh
npx @frontic/ui add price
```

Built from: [Badge](https://ui.frontic.com/components/ui/badge.md), [Format](https://ui.frontic.com/lib/format.md)

Used by: [Cart Summary](https://ui.frontic.com/components/sections/cart-summary.md), [Filters](https://ui.frontic.com/components/sections/filters.md), [Order Summary](https://ui.frontic.com/components/sections/order-summary.md), [Payment Method](https://ui.frontic.com/components/sections/payment-method.md), [Product Card Basic](https://ui.frontic.com/components/sections/product-card-basic.md), [Product Card Featured](https://ui.frontic.com/components/sections/product-card-featured.md), [Product Card Gallery](https://ui.frontic.com/components/sections/product-card-gallery.md), [Product Card Horizontal](https://ui.frontic.com/components/sections/product-card-horizontal.md), [Product Card Hover](https://ui.frontic.com/components/sections/product-card-hover.md), [Product Card Minimal](https://ui.frontic.com/components/sections/product-card-minimal.md), [Product Options](https://ui.frontic.com/components/sections/product-options.md), [Promo Code](https://ui.frontic.com/components/sections/promo-code.md), [Recent Orders](https://ui.frontic.com/components/sections/recent-orders.md), [Return Request](https://ui.frontic.com/components/sections/return-request.md), [Search Suggestions](https://ui.frontic.com/components/sections/search-suggestions.md), [Cart Compact](https://ui.frontic.com/components/templates/cart-compact.md), [Cart Full](https://ui.frontic.com/components/templates/cart-full.md), [Cart Item Compact](https://ui.frontic.com/components/templates/cart-item-compact.md), [Cart Item Detailed](https://ui.frontic.com/components/templates/cart-item-detailed.md), [Cart Item Minimal](https://ui.frontic.com/components/templates/cart-item-minimal.md), [Cart Minimal](https://ui.frontic.com/components/templates/cart-minimal.md), [Checkout Page](https://ui.frontic.com/components/templates/checkout-page.md)

npm packages: `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { Price } from '@/components/ui/price'
</script>

<template>
  <Price :price="{ amount: 9000, currency: 'EUR', ref: 11100 }">
    <template #hint>Incl. VAT, plus <a href="#">shipping</a></template>
  </Price>
</template>
```

## Anatomy

```vue
<Price>
  <PriceDiscount />
  <PriceRef />
  <PriceAmount />
  <PriceHint />
</Price>
```

## API reference

### Price

Shows a price, with the original price and discount percentage when reduced. Renders a `<span>`.

`data-slot="price"` · `Price.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `PriceSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The text size of the price. |
| `align` | `PriceAlign`: `'start' \| 'end'` | `'start'` | Which side the badge, the prices and the hint line up on. |
| `price` (required) | `PriceType` |  | The price to show: `amount` and `ref` in minor units such as cents, `currency` as an ISO 4217 code. |
| `locale` | `string` |  | The locale that formats the price, e.g. `de-DE`; the ConfigProvider's when left out. |
| `showRef` | `boolean` | `true` | Shows the reference price struck through when it is higher than the amount. |
| `showDiscount` | `boolean` | `true` | Shows the saving as a percentage next to a discounted price. |
| `showHint` | `boolean` | `true` | Shows the hint below the price when the hint slot holds one. |
| `refLabel` | `string` | `'Was'` | Read out before the original price of a reduced product; hidden on screen. |
| `amountLabel` | `string` | `'Now'` | Read out before the current price of a reduced product; hidden on screen. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `hint` |  | A note below the price, such as tax and shipping. |

### PriceAmount

Shows the current price of the product. Renders a `<span>`.

`data-slot="price-amount"` · `PriceAmount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### PriceRef

Shows the original price of a discounted product. Renders a `<span>`.

`data-slot="price-ref"` · `PriceRef.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### PriceDiscount

Shows the percentage saved against the original price. Renders a `<span>`.

`data-slot="price-discount"` · `PriceDiscount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ percent: number; }` | Replaces the discount badge; gets the percentage saved. |

### PriceHint

Holds a note below the price, such as tax and shipping, which may link on. Renders a `<span>`.

`data-slot="price-hint"` · `PriceHint.vue`

Styling: `price-hint`, which extends [`link-foreground`](https://ui.frontic.com/DESIGN.md#link-foreground). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

## Exports

```ts
import { formatPrice, type PriceType, type PriceSize, type PriceAlign } from '@/components/ui/price'
```

- `formatPrice`, `PriceType`: from [Format](https://ui.frontic.com/lib/format.md)
- `PriceSize`: `'sm' \| 'md' \| 'lg'`
- `PriceAlign`: `'start' \| 'end'`

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`price`](https://ui.frontic.com/DESIGN.md#price) |
| `row` | [`price-row`](https://ui.frontic.com/DESIGN.md#price-row) |
| `hint` | [`price-hint`](https://ui.frontic.com/DESIGN.md#price-hint) |
| `strike` | [`price-strike`](https://ui.frontic.com/DESIGN.md#price-strike) |
| `discount` | [`price-discount`](https://ui.frontic.com/DESIGN.md#price-discount) |

## Source

`components/ui/price/Price.vue`

```vue
<script lang="ts">
/** Shows a price, with the original price and discount percentage when reduced. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed, provide, toRef } from 'vue'
import { cva } from 'class-variance-authority'
import { type PriceType, formatPrice, percentOf } from '@library/lib/format'
import { type PriceAlign, type PriceSize, PRICE_INJECTION_KEY } from '.'
import PriceAmount from './PriceAmount.vue'
import PriceRef from './PriceRef.vue'
import PriceDiscount from './PriceDiscount.vue'
import PriceHint from './PriceHint.vue'
import { useLocale } from 'reka-ui'

const root = cva('fui-price tabular-nums', {
  variants: {
    size: { sm: 'fui-price-sm', md: 'fui-price-md', lg: 'fui-price-lg' },
    align: { start: 'fui-price-start', end: 'fui-price-end' },
  },
  defaultVariants: { size: 'md', align: 'start' },
})

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes['class']
    /** The text size of the price. */
    size?: PriceSize
    /** Which side the badge, the prices and the hint line up on. */
    align?: PriceAlign
    /** The price to show: `amount` and `ref` in minor units such as cents, `currency` as an ISO 4217 code. */
    price: PriceType
    /** The locale that formats the price, e.g. `de-DE`; the ConfigProvider's when left out. */
    locale?: string
    /** Shows the reference price struck through when it is higher than the amount. */
    showRef?: boolean
    /** Shows the saving as a percentage next to a discounted price. */
    showDiscount?: boolean
    /** Shows the hint below the price when the hint slot holds one. */
    showHint?: boolean
    /** Read out before the original price of a reduced product; hidden on screen. */
    refLabel?: string
    /** Read out before the current price of a reduced product; hidden on screen. */
    amountLabel?: string
  }>(),
  {
    size: 'md',
    align: 'start',
    showRef: true,
    showDiscount: true,
    showHint: true,
    refLabel: 'Was',
    amountLabel: 'Now',
  },
)
const locale = useLocale(toRef(() => props.locale))

const fmt = (amount: number) => formatPrice({ ...props.price, amount }, locale.value)

const amount = computed(() => props.price.amount ?? 0)
const refPrice = computed(() => props.price.ref ?? 0)
const isDiscounted = computed(() => props.showRef && refPrice.value > amount.value)
const formattedPrice = computed(() => fmt(amount.value))
const formattedRef = computed(() => (isDiscounted.value ? fmt(refPrice.value) : ''))
const discountPercent = computed(() => (isDiscounted.value && props.showDiscount ? percentOf(refPrice.value - amount.value, refPrice.value) : 0))

defineSlots<{
  /** Replaces the badge and the price row; compose PriceDiscount, PriceRef and PriceAmount in any order. */
  default?: () => unknown
  /** A note below the price, such as tax and shipping. */
  hint?: () => unknown
}>()

provide(PRICE_INJECTION_KEY, {
  formattedPrice,
  formattedRef,
  isDiscounted,
  discountPercent,
  size: computed(() => props.size),
  refLabel: computed(() => props.refLabel),
  amountLabel: computed(() => props.amountLabel),
})
</script>

<template>
  <span data-slot="price" :class="[root({ size, align }), props.class]">
    <slot>
      <PriceDiscount />
      <span data-slot="price-row" class="fui-price-row inline-flex flex-wrap items-baseline">
        <PriceRef />
        <PriceAmount />
      </span>
    </slot>
    <PriceHint v-if="props.showHint && $slots.hint"><slot name="hint" /></PriceHint>
  </span>
</template>
```

`components/ui/price/PriceAmount.vue`

```vue
<script lang="ts">
/** Shows the current price of the product. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { PRICE_INJECTION_KEY } from '.'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()

const ctx = inject(PRICE_INJECTION_KEY)
</script>

<template>
  <span v-if="ctx" data-slot="price-amount" :class="props.class">
    <span v-if="ctx.isDiscounted.value" class="sr-only">{{ ` ${ctx.amountLabel.value} ` }}</span>
    <slot>{{ ctx.formattedPrice.value }}</slot>
  </span>
</template>
```

`components/ui/price/PriceDiscount.vue`

```vue
<script lang="ts">
/** Shows the percentage saved against the original price. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { Badge } from '@library/components/ui/badge'
import { PRICE_INJECTION_KEY } from '.'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()

const ctx = inject(PRICE_INJECTION_KEY)

defineSlots<{
  /** Replaces the discount badge; gets the percentage saved. */
  default?: (props: { percent: number }) => unknown
}>()
</script>

<template>
  <span v-if="ctx?.isDiscounted.value && ctx.discountPercent.value > 0" data-slot="price-discount" :class="['fui-price-discount', props.class]">
    <slot :percent="ctx.discountPercent.value">
      <Badge variant="discount">-{{ ctx.discountPercent.value }}%</Badge>
    </slot>
  </span>
</template>
```

`components/ui/price/PriceHint.vue`

```vue
<script lang="ts">
/** Holds a note below the price, such as tax and shipping, which may link on. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'span' },
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="price-hint" :class="['fui-price-hint', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/price/PriceRef.vue`

```vue
<script lang="ts">
/** Shows the original price of a discounted product. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { cva } from 'class-variance-authority'
import { PRICE_INJECTION_KEY } from '.'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()

const ctx = inject(PRICE_INJECTION_KEY)

const strike = cva('fui-price-strike', {
  variants: {
    size: { sm: 'fui-price-strike-sm', md: 'fui-price-strike-md', lg: 'fui-price-strike-lg' },
  },
  defaultVariants: { size: 'md' },
})
</script>

<template>
  <span v-if="ctx?.isDiscounted.value" data-slot="price-ref" :class="[strike({ size: ctx.size.value }), props.class]">
    <span class="sr-only">{{ ` ${ctx.refLabel.value} ` }}</span>
    <slot>{{ ctx.formattedRef.value }}</slot>
  </span>
</template>
```

`components/ui/price/index.ts`

```ts
import type { ComputedRef, InjectionKey } from 'vue'

export { default as Price } from './Price.vue'
export { default as PriceAmount } from './PriceAmount.vue'
export { default as PriceRef } from './PriceRef.vue'
export { default as PriceDiscount } from './PriceDiscount.vue'
export { default as PriceHint } from './PriceHint.vue'
export { formatPrice, type PriceType } from '@library/lib/format'

export interface PriceContext {
  formattedPrice: ComputedRef<string>
  formattedRef: ComputedRef<string>
  isDiscounted: ComputedRef<boolean>
  discountPercent: ComputedRef<number>
  size: ComputedRef<PriceSize>
  refLabel: ComputedRef<string>
  amountLabel: ComputedRef<string>
}

export const PRICE_INJECTION_KEY = Symbol('price-context') as InjectionKey<PriceContext>

export type PriceSize = 'sm' | 'md' | 'lg'

export type PriceAlign = 'start' | 'end'
```
