PriceTag
Rounded-box price overlay for house cards, galleries, and buy-side comparison surfaces.
Quick Preview
Usage
PriceTag is display only. It prints value and description as given and never composes a sentence, picks a format, or decides that a line is too small to be worth showing.
import { PriceTag } from "@giddaa-housing/ui/price-tag";
<PriceTag value="N78,650,000" size="md" />Wording, currency conversion and number formatting all belong to the app, which is the only place that knows the locale, the product's voice, and what the second line is actually for.
Deprecated props
amount, shortAmount, format, monthlyPayment and monthlyPaymentLabel still work and keep their old behaviour exactly. They are removed in the next major — see Migrating.
Examples
Size
Use size from xxs through lg to match image density and overlay prominence.
Formatting
Format the number before you pass it. Intl.NumberFormat already knows the locale and the currency, and it is the app that knows how much room the card has.
Description
description is a second line under the price, printed as given. It is a slot, not a monthly-payment field — the same space can carry financing, scarcity, or a fee note, and PriceTag has no opinion about which.
No
Price only
Financing
Your wording
Scarcity
Same slot, other job
Currency Dropdown
Pass currencies to turn the tag into a dropdown trigger. Each option is a currency, a value, and an optional description.
No
Plain price pill
Yes
Chevron to switch currency
Open
Currency popover open
Selecting a row calls onCurrencyChange with the option. PriceTag does not hold the selection — pass selectedCurrency to say which row is current, so the tag stays in step with whatever the app decided.
Custom rows
renderMenuOption replaces a row's contents. PriceTag keeps the button, its selected state and its keyboard behaviour, so a custom row can't quietly lose them.
Options carry whatever else you need. PriceTag infers the option type from currencies, so fields it knows nothing about — a rate, an id, a country code — arrive in renderMenuOption and onCurrencyChange typed, without a cast or a generic to write out.
<PriceTag
currencies={[{ currency: "USD", value: "$51,200", rate: 1535.16 }]}
renderMenuOption={(option) => <span>{option.rate}</span>} // number
/>Custom rows
Live rate from the option's own fields
Migrating
Everything below still works. Move at your own pace; the old props go in the next major.
| Old | New |
|---|---|
amount="N78,650,000" | value="N78,650,000" |
shortAmount + format="shortened" | Format before passing, then value |
monthlyPayment="N845" | description="Low as N845 monthly" |
monthlyPaymentLabel="From" | Part of your description string |
option { code, amount } | option { currency, value } |
option { monthlyPayment } | option { description } |
Two differences worth knowing when you move:
- The monthly line was hidden below
size="md". Adescriptionyou pass deliberately renders at every size, because deciding a line isn't worth the room is the app's call. - The old props built the sentence for you —
"Low as N845 monthly"on the tag,"As low as N845 per month"in the dropdown, which never quite matched.descriptionis printed exactly as given, so write the line you want.
New props win over the ones they replace, so an option or a tag can be migrated one field at a time.
Tokens
| Slot | Token | Notes |
|---|---|---|
| Surface | bg-surface-brand | Tag background resolves green in Light and deep green in Dark. |
| Label | text-fg-on-brand | Stays readable on either theme. |
| Popover | bg-surface-overlay, shadow-3 | Currency comparison panel uses overlay elevation. |
Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
value | ReactNode | - | The price, exactly as it should read. |
description | ReactNode | - | A second line under the price. Your words, printed as given. |
size | "xxs" | "xs" | "sm" | "md" | "lg" | "md" | Tag scale. Also sets the corner radius, which grows with the size. |
currencies | PriceTagCurrencyOption[] | - | Turns the tag into a dropdown trigger. |
selectedCurrency | string | - | Which option's currency is current. |
onCurrencyChange | (option: PriceTagMenuOption) => void | - | Called when a row is selected. |
renderMenuOption | (option: PriceTagMenuOption) => ReactNode | - | Replaces a row's contents, keeping its button and behaviour. |
open / defaultOpen / onOpenChange | popover state | - | Controls the dropdown. |
popoverClassName | string | - | Overrides on the popover surface. |
className | string | - | Local layout or spacing overrides. |
amount | string | - | Deprecated. Use value. |
shortAmount / format | string / "full" | "shortened" | "full" | Deprecated. Format before passing. |
monthlyPayment / monthlyPaymentLabel | string | "Low as" | Deprecated. Use description. |
| native button props | React button props | - | Passed to the trigger, or to the static tag. |
PriceTagCurrencyOption is { currency, value, description? }, plus the deprecated code, amount, shortAmount, monthlyPayment and monthlyPaymentLabel. PriceTagMenuOption<TOption> is that option after the deprecated fields have been folded in, with currency and value resolved and a selected boolean — the original object is spread in, so code still reading code or amount off it keeps working.
Both PriceTagMenuOption and PriceTagProps are generic over your option type and default to the base one, so existing code referring to them unparameterised keeps compiling. You rarely need to name the type parameter: it is inferred from currencies.
Accessibility
- Plain tags render as non-interactive text.
- Dropdown tags render as buttons and preserve Base UI popover keyboard behavior.
- Keep the
currencyon each option explicit so screen reader users can distinguish rows. IfrenderMenuOptiondrops the code from the visible row, give the row an accessible name another way. descriptionis read out with the price, so keep it short and meaningful rather than decorative.