Price
A product price: the current amount, an optional struck-through compare-at amount and an optional savings badge in one baseline-aligned run, formatted by currency and locale through money-value.
Preview
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground">{{ __('On sale, with derived savings') }}</span>
<x-ui.price amount="118" compare-at="148" show-savings size="lg" />
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground">{{ __('Sizes') }}</span>
<div class="flex flex-wrap items-baseline gap-6">
<x-ui.price amount="96" size="sm" />
<x-ui.price amount="96" size="md" />
<x-ui.price amount="96" size="lg" />
<x-ui.price amount="96" size="xl" />
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground">{{ __('Locale, currency and a suffix') }}</span>
<div class="flex flex-wrap items-baseline gap-6">
<x-ui.price amount="1299" currency="EUR" locale="nl-NL" :suffix="__('/ month')" />
<x-ui.price amount="1299" currency="JPY" locale="ja-JP" />
<x-ui.price :amount="__('From $9')" :suffix="__('each')" />
</div>
</div>
</div>
Installation
php artisan ui:add price
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Price (`price`)
A product price: the current amount, an optional struck-through compare-at amount and an optional savings badge in one baseline-aligned run, formatted by currency and locale through money-value.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add price
```
## Usage
```blade
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground">{{ __('On sale, with derived savings') }}</span>
<x-ui.price amount="118" compare-at="148" show-savings size="lg" />
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground">{{ __('Sizes') }}</span>
<div class="flex flex-wrap items-baseline gap-6">
<x-ui.price amount="96" size="sm" />
<x-ui.price amount="96" size="md" />
<x-ui.price amount="96" size="lg" />
<x-ui.price amount="96" size="xl" />
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-foreground">{{ __('Locale, currency and a suffix') }}</span>
<div class="flex flex-wrap items-baseline gap-6">
<x-ui.price amount="1299" currency="EUR" locale="nl-NL" :suffix="__('/ month')" />
<x-ui.price amount="1299" currency="JPY" locale="ja-JP" />
<x-ui.price :amount="__('From $9')" :suffix="__('each')" />
</div>
</div>
</div>
```
## Props
- `amount` (string|int|float|null, default `null`) — The current price. A numeric value ("118", "118.00", 118) is formatted by currency and locale through money-value; any other string is shown verbatim.
- `compareAt` (string|int|float|null, default `null`) — The original price on a sale, rendered struck through with a visually-hidden "Original price" prefix. Same numeric-or-verbatim rule as `amount`.
- `currency` (string, default `USD`) — ISO 4217 code handed to money-value for numeric amounts.
- `locale` (string, default `en-US`) — BCP 47 locale that decides symbol side, grouping and decimal separators.
- `size` (sm|md|lg|xl, default `md`) — Scale of the current amount; the compare-at sits one step below it.
- `showSavings` (bool, default `false`) — Appends a "Save N%" badge computed from two numeric amounts when compareAt exceeds amount.
- `suffix` (string|null, default `null`) — Muted trailing text such as "/ month" or "each".
## Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- Showing what a product costs on a card, a product page, a cart line or a sticky buy bar.
- A sale needs the original price struck through beside the current one, read correctly by AT ("Sale price … Original price …").
- You want the discount percentage derived rather than hard-coded: show-savings.
## Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- Presenting a single money figure that is not a price (a balance, a total, a fee) — compose <x-ui.money-value> directly.
- Laying out plan tiers with a period and features — that is <x-ui.pricing-card>.
## Anti-patterns
- Adding display ornament without informational value
## Rules
- Use the `<brok:price>` tag (or `<x-ui.price>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/price
- Registry JSON (files, props, contract): https://brokui.dev/r/open/price.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
Long Content
<div class="max-w-xs">
<x-ui.price amount="1234567890.99" compare-at="1999999999.99" currency="EUR" locale="de-DE" show-savings size="xl" :suffix="__('per seat, billed annually, taxes calculated at checkout')" />
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| amount | string | int | float | null | null | The current price. A numeric value ("118", "118.00", 118) is formatted by currency and locale through money-value; any other string is shown verbatim. |
| compareAt | string | int | float | null | null | The original price on a sale, rendered struck through with a visually-hidden "Original price" prefix. Same numeric-or-verbatim rule as `amount`. |
| currency | string | USD | ISO 4217 code handed to money-value for numeric amounts. |
| locale | string | en-US | BCP 47 locale that decides symbol side, grouping and decimal separators. |
| size | sm | md | lg | xl | md | Scale of the current amount; the compare-at sits one step below it. |
| showSavings | bool | false | Appends a "Save N%" badge computed from two numeric amounts when compareAt exceeds amount. |
| suffix | string | null | null | Muted trailing text such as "/ month" or "each". |
Slots
Default Blade slot only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Server-rendered Blade, no JavaScript.
- Numeric amounts never touch floating-point maths: money-value formats them as strings.
- Declares registry capability flags: a11y, authoredStateFixtures, responsive, rtl, darkMode, localized.
Guidance
Present data for rapid scanning.
Use when
- Use to summarize, sequence, or present data so users can scan it quickly.
- Showing what a product costs on a card, a product page, a cart line or a sticky buy bar.
- A sale needs the original price struck through beside the current one, read correctly by AT ("Sale price … Original price …").
- You want the discount percentage derived rather than hard-coded: show-savings.
Avoid when
- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- Presenting a single money figure that is not a price (a balance, a total, a fee) — compose <x-ui.money-value> directly.
- Laying out plan tiers with a period and features — that is <x-ui.pricing-card>.
Use instead
- Table for exact comparison
- Plain text for a single value
Anti-patterns
- Adding display ornament without informational value
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- Line-through is visual only; the compare-at amount carries a visually-hidden 'Original price' prefix and the current one 'Sale price' whenever both render.
- The run is inline-flex with baseline alignment, so wrapping keeps the amounts readable.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'amount' => null,
'compareAt' => null,
'currency' => 'USD',
'locale' => 'en-US',
'size' => 'md',
'showSavings' => false,
'suffix' => null,
])
{{--
Price: the current amount, an optional struck-through compare-at amount and
an optional savings badge, in one baseline-aligned run.
Each amount is a <x-ui.money-value>, so integer/decimal strings format by
currency and locale without floating-point maths. A pre-formatted string
("$118.00", "From €9") is shown verbatim instead — handy for content that
arrives already formatted, but the theme and locale cannot reach into it,
so prefer numeric amounts.
The visual line-through is not enough for AT: the compare-at amount is
prefixed with a visually-hidden "Original price" and the current one with
"Sale price" whenever both are present, so the sentence reads correctly.
--}}
@php
$size = in_array($size, ['sm', 'md', 'lg', 'xl'], true) ? $size : 'md';
// money-value's own scale carries the current amount; the compare-at sits
// one step below it so the discount reads at a glance.
$compareSize = ['sm' => 'sm', 'md' => 'sm', 'lg' => 'md', 'xl' => 'lg'][$size];
$sizes = [
'sm' => ['gap' => 'gap-1', 'text' => 'text-xs', 'compare' => 'text-xs', 'suffix' => 'text-xs'],
'md' => ['gap' => 'gap-2', 'text' => 'text-sm font-semibold', 'compare' => 'text-sm', 'suffix' => 'text-xs'],
'lg' => ['gap' => 'gap-2', 'text' => 'text-lg font-semibold', 'compare' => 'text-sm', 'suffix' => 'text-sm'],
'xl' => ['gap' => 'gap-2', 'text' => 'text-2xl font-semibold tracking-tight', 'compare' => 'text-base', 'suffix' => 'text-sm'],
];
$s = $sizes[$size];
$isNumeric = static fn ($v): bool => $v !== null && preg_match('/^-?\d+(?:\.\d+)?$/', (string) $v) === 1;
$amountIsNumeric = $isNumeric($amount);
$compareIsNumeric = $isNumeric($compareAt);
$hasCompare = $compareAt !== null && $compareAt !== '';
// Savings can only be derived from two numeric amounts.
$savings = null;
if ($showSavings && $amountIsNumeric && $compareIsNumeric && (float) $compareAt > (float) $amount) {
$savings = (int) round((1 - (float) $amount / (float) $compareAt) * 100);
}
@endphp
<span
data-slot="price"
data-size="{{ $size }}"
@if ($hasCompare) data-on-sale="true" @endif
{{ $attributes->merge(['class' => 'inline-flex min-w-0 flex-wrap items-baseline '.$s['gap']]) }}
>
@if ($hasCompare)
<span class="sr-only">{{ __('Sale price') }}</span>
@endif
<span data-slot="price-amount" class="{{ 'whitespace-nowrap tabular-nums text-foreground '.$s['text'] }}">
@if ($amountIsNumeric)
<x-ui.money-value :amount="$amount" :currency="$currency" :locale="$locale" :size="$size" class="font-semibold" />
@else
{{ $amount }}
@endif
</span>
@if ($suffix)
<span data-slot="price-suffix" class="{{ 'text-muted-foreground '.$s['suffix'] }}">{{ $suffix }}</span>
@endif
@if ($hasCompare)
<s data-slot="price-compare-at" class="{{ 'whitespace-nowrap text-muted-foreground '.$s['compare'] }}">
<span class="sr-only">{{ __('Original price') }}</span>
@if ($compareIsNumeric)
<x-ui.money-value :amount="$compareAt" :currency="$currency" :locale="$locale" :size="$compareSize" class="!font-normal !text-muted-foreground" />
@else
{{ $compareAt }}
@endif
</s>
@endif
@if ($savings !== null)
<span data-slot="price-savings" class="inline-flex self-center">
<x-ui.badge variant="destructive" size="sm">{{ __('Save :percent%', ['percent' => $savings]) }}</x-ui.badge>
</span>
@endif
</span>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0