Components
PriceDisplay
StableRenders holiday pricing with contextual detail — currency, per-person or family basis, savings callout, board basis, nights, departure note, local tax disclosure, and ATOL badge.
Purpose
PriceDisplay is the canonical way to show holiday pricing across the Kuoni site. It wraps the price numeral in the context a customer needs to evaluate it: the pricing basis (per person, couple, or family), an optional original price for savings callouts, board basis and night count for holiday cards, departure context, local tax disclosures, and ATOL protection status.
Use PriceDisplay when:
- Showing a price on an offer card, search result tile, or holiday grid — use
size="sm". - Showing a price in a booking sidebar, sticky summary, or comparison panel — use
size="md". - Showing a price in the hero section of a holiday detail page or booking widget — use
size="lg".
Do not use PriceDisplay for:
- Non-holiday prices (e.g. insurance, single supplements shown as additions in a breakdown table — use plain text there).
- Price ranges — this component shows one definitive price with an optional 'From' prefix; it is not a range component.
Usage
Use PriceDisplay whenever a holiday price is the primary figure a customer needs to evaluate, on cards, sidebars, or hero sections. For secondary or itemised amounts inside a breakdown table (taxes, supplements, add-ons), use plain text instead — reserving PriceDisplay's visual weight for the headline figure.
It typically appears at the top of a holiday or offer card, in a booking sidebar summary, or in the hero of a holiday detail page — size scales with how prominent the price should be in that placement.
Anatomy
10 nights · Full board · from Manchester, Nov 2026
€53pp payable locally
- 1"From" prefix — 12 px, tertiary grey (#999999). Always present. Communicates that this is the lowest available price, not a fixed quote.
- 2Currency symbol — Rendered immediately before the price numeral with no space. Defaults to £. Inherits the price numeral size and weight.
- 3Price numeral — Bold (700). sm=18px, md=22px, lg=28px. Dark #1A1A1A. The most visually prominent element — the eye lands here first.
- 4Basis suffix — 12 px, tertiary grey (#999999). Defines what the price covers: "per person", "per couple", "per family of 4". Never omit this.
- 5Context row — 12–14 px, secondary grey (#666666). Optional. Shows nights · board basis · departure note in a single line.
- 6Local tax note — 12 px, secondary grey (#666666) with an Info icon. Discloses charges payable at destination.
- 7ATOL badge — Small orange badge reading "ATOL Protected". Shown only when atolProtected=true.
Variants
Simple
The baseline — price numeral with "From" prefix and basis suffix. Use on compact offer cards where vertical space is limited.
Savings (was-price)
Pass wasAmount to show the original price struck through above the current price. The saving is implicit from the two numbers; do not add a "Save £X" label inside this component — use a Badge with variant="promotion" alongside it instead.
The struck-through was-price is hidden from assistive technology (aria-hidden="true"). The root element carries a full aria-label that reads: "Was £2,199, now From £1,759 per person".
Family
Set basis="family" and familySize to show family pricing. The suffix becomes "per family of 4" (or "per family" when familySize is omitted).
Detailed
For holiday detail pages and sidebars — combine board basis, nights, departure note, local tax disclosure, and ATOL badge.
10 nights · Full board · from Manchester, Nov 2026
€53pp payable locally
Sizes
Size controls the price numeral scale. All other elements (prefix, suffix, context row) scale proportionally.
| Size | Price numeral | Prefix / suffix | Context row | Placement |
|---|---|---|---|---|
sm | 18 px | 11 px | 12 px | Offer cards, grid tiles |
md | 22 px | 12 px | 13 px | Booking sidebars, summary panels |
lg | 28 px | 13 px | 14 px | Holiday detail hero, booking widget |
States
PriceDisplay is a display-only component with no interactive states. It does not respond to hover, focus, or click. When a price is unavailable or still loading, do not render PriceDisplay — use a skeleton placeholder instead so the layout does not shift when the price arrives.
{/* While loading */}
<div className="h-7 w-28 animate-pulse rounded bg-[#F5F5F5]" aria-hidden="true" />
{/* Once resolved */}
<PriceDisplay amount={1759} basis="person" />
Best practices
Always include the pricing basis (per person / couple / family of N) using the basis prop.
The basis suffix tells the customer exactly what the price covers before they click through.
Show a raw number without a currency symbol or pricing basis.
A bare number with no currency or basis forces the customer to guess — and they will guess wrong.
Use wasAmount to show the original price alongside the current price when a saving applies.
The savings variant shows the original and current price — the discount is self-evident.
Manually add a 'Save X%' string next to PriceDisplay — use a Badge with variant='promotion' instead.
An invented percentage figure next to PriceDisplay creates a credibility problem if the numbers do not add up.
Match the size prop to the component placement: sm for cards, md for sidebars, lg for hero.
sm on cards keeps the card compact; lg on the detail page gives the price the emphasis it deserves.
Use the same size prop in every context regardless of the surrounding layout.
An 18 px price numeral disappears on a hero page. A 28 px price overwhelms a compact card grid.
Use localTaxNote for charges payable at the destination (resort fees, tourist taxes, visas).
Disclosing local taxes near the displayed price meets UK trading standards guidance and builds trust.
Omit locally payable charges from the displayed price without any disclosure.
Hiding additional costs until the checkout funnel damages trust and increases abandonment.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| amount* | number | — | The main display price. Rendered with toLocaleString() for thousand separators. |
| wasAmount | number | — | Original price before the saving. When provided and greater than amount, renders a struck-through was-price above the main price. |
| currency | string | '£' | Currency symbol prepended to price numerals with no space. Use the symbol, not the ISO code. |
| basis | 'person' | 'family' | 'couple' | 'person' | Pricing basis. Controls the suffix: 'per person', 'per couple', or 'per family of N' (when familySize is also set). |
| familySize | number | — | Party size when basis='family'. Produces 'per family of 4'. Omit to show 'per family' without a count. |
| boardBasis | string | — | Board inclusion label shown in the context row, e.g. 'Full board', 'B&B', 'All inclusive'. |
| nights | number | — | Number of nights shown in the context row, e.g. 7. Rendered as "7 nights". |
| departureNote | string | — | Departure context shown in the context row, e.g. 'from London Heathrow, Oct 2026'. |
| localTaxNote | string | — | Disclosure text for charges payable at the destination, e.g. '€53pp payable locally'. Rendered with an Info icon. |
| atolProtected | boolean | false | Shows the ATOL Protected badge when true. Only set this when the booking is genuinely ATOL protected. |
| size | 'sm' | 'md' | 'lg' | 'md' | Controls price numeral scale and surrounding text sizes. sm for cards, md for sidebars, lg for hero detail pages. |
| className | string | — | Additional Tailwind classes on the root element. Use for margin, alignment, or width overrides. |
Accessibility
Semantic price text
The root element of PriceDisplay carries a complete aria-label that encodes the full price sentence in plain language. This means screen readers announce a single, grammatically correct string rather than reading out the visually separated fragments ("From", "£", "1,759", "per person") individually.
{/* aria-label output: "From £1,759 per person" */}
<PriceDisplay amount={1759} basis="person" />
{/* aria-label output: "Was £2,199, now From £1,759 per person" */}
<PriceDisplay amount={1759} wasAmount={2199} basis="person" />
{/* aria-label output: "From £10,299 per family of 4" */}
<PriceDisplay amount={10299} basis="family" familySize={4} />
All child elements inside the component are marked aria-hidden="true" to prevent double-reading.
Struck-through was-price
The visual struck-through format communicates a saving at a glance for sighted users. Without accessible text, screen readers would announce the old price as an ordinary number with no indication that it is superseded. The aria-label on the root element handles this by including "Was £X, now From £Y" when wasAmount is present.
Local tax and ATOL disclosure
The localTaxNote and ATOL badge are visible text and do not require additional ARIA. Ensure the note is also surfaced somewhere in the booking flow (e.g. a tooltip or expandable section) so it is not missed when the card is skipped.
Colour contrast
All text in PriceDisplay uses colours that meet WCAG 2.1 AA at the smallest size (sm):
| Element | Colour | Background | Ratio | WCAG |
|---|---|---|---|---|
| Price numeral | #1A1A1A | #FFFFFF | 17.4:1 | ✅ AAA |
| "From" prefix / basis suffix | #999999 | #FFFFFF | 2.85:1 | ⚠ decorative — label carries meaning |
| Context row | #666666 | #FFFFFF | 5.74:1 | ✅ AA |
| Was-price (struck) | #999999 | #FFFFFF | 2.85:1 | decorative — meaning in aria-label |
Related components
- FlightInfo — often shown alongside PriceDisplay in a booking summary or itinerary card.