KKUONIDesign System

Components

PriceDisplay

Stable

Renders 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

ATOLProtected
  1. 1
    "From" prefix12 px, tertiary grey (#999999). Always present. Communicates that this is the lowest available price, not a fixed quote.
  2. 2
    Currency symbolRendered immediately before the price numeral with no space. Defaults to £. Inherits the price numeral size and weight.
  3. 3
    Price numeralBold (700). sm=18px, md=22px, lg=28px. Dark #1A1A1A. The most visually prominent element — the eye lands here first.
  4. 4
    Basis suffix12 px, tertiary grey (#999999). Defines what the price covers: "per person", "per couple", "per family of 4". Never omit this.
  5. 5
    Context row12–14 px, secondary grey (#666666). Optional. Shows nights · board basis · departure note in a single line.
  6. 6
    Local tax note12 px, secondary grey (#666666) with an Info icon. Discloses charges payable at destination.
  7. 7
    ATOL badgeSmall 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

ATOLProtected

Sizes

Size controls the price numeral scale. All other elements (prefix, suffix, context row) scale proportionally.

sm — offer cards
md — sidebars
lg — hero / detail
SizePrice numeralPrefix / suffixContext rowPlacement
sm18 px11 px12 pxOffer cards, grid tiles
md22 px12 px13 pxBooking sidebars, summary panels
lg28 px13 px14 pxHoliday 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

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

Omit locally payable charges from the displayed price without any disclosure.

Hiding additional costs until the checkout funnel damages trust and increases abandonment.

Props

PropTypeDefaultDescription
amount*numberThe main display price. Rendered with toLocaleString() for thousand separators.
wasAmountnumberOriginal price before the saving. When provided and greater than amount, renders a struck-through was-price above the main price.
currencystring'£'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).
familySizenumberParty size when basis='family'. Produces 'per family of 4'. Omit to show 'per family' without a count.
boardBasisstringBoard inclusion label shown in the context row, e.g. 'Full board', 'B&B', 'All inclusive'.
nightsnumberNumber of nights shown in the context row, e.g. 7. Rendered as "7 nights".
departureNotestringDeparture context shown in the context row, e.g. 'from London Heathrow, Oct 2026'.
localTaxNotestringDisclosure text for charges payable at the destination, e.g. '€53pp payable locally'. Rendered with an Info icon.
atolProtectedbooleanfalseShows 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.
classNamestringAdditional 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):

ElementColourBackgroundRatioWCAG
Price numeral#1A1A1A#FFFFFF17.4:1✅ AAA
"From" prefix / basis suffix#999999#FFFFFF2.85:1⚠ decorative — label carries meaning
Context row#666666#FFFFFF5.74:1✅ AA
Was-price (struck)#999999#FFFFFF2.85:1decorative — meaning in aria-label
  • FlightInfo — often shown alongside PriceDisplay in a booking summary or itinerary card.