<EstimateBreakdown>
Hierarchical pre-reservation pricing breakdown. Reads the current
ticket-type selection via useSelectionEstimate(), walks each
ticket type's active waves cheapest-first, and renders the result
as a tree: per ticket type → wave-split lines → subtotal → grand
total + always-visible disclaimer.
Layout-neutral by design — ships no position, no
sticky/fixed behavior, no opinions about where on the page it
lives. Inline panel, sticky drawer, sidebar, modal — all are the
consumer's choice.
Import
import { EstimateBreakdown } from "@feelrift/react";
Basic usage
import {
AvailabilityList,
EstimateBreakdown,
RiftEvent,
RiftProvider,
} from "@feelrift/react";
<RiftProvider>
<RiftEvent eventId="evt_summerfest_2026">
<AvailabilityList />
<EstimateBreakdown />
</RiftEvent>
</RiftProvider>;
Props
Accepts all HTMLAttributes<HTMLDivElement> plus:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Swap the root element via Radix Slot. |
children | ReactNode | ((estimate: SelectionEstimate) => ReactNode) | No | — | Override the default rendering. Function form receives the live SelectionEstimate. |
emptyFallback | ReactNode | No | Locale string estimate.empty in a div | Rendered when no quantity is selected. Pass null to render nothing. |
now | () => Date | No | () => new Date() | Wall-clock source forwarded to useSelectionEstimate. Use to freeze time in tests / Storybook. |
disclaimer | ReactNode | null | No | Locale string estimate.disclaimer | Override the disclaimer text near the total. Pass null to hide entirely — not recommended. |
className | string | No | — | Appended to the SDK's rift-estimate-breakdown class. |
…rest | HTMLAttributes<HTMLDivElement> | No | — | Forwarded to the root element. |
disclaimer
The default text reads:
"We pick the cheapest available tier first. Final price (including any fees) is confirmed when you reserve."
The mention of "any fees" is deliberate — server-side modifiers
(platform fees, taxes, discounts applied in the pricing engine)
aren't visible in the client-side estimate. They appear in the
reservation/checkout response's breakdown.steps. The disclaimer
sets that expectation up front so the post-reservation total
isn't a surprise.
Override the text when your jurisdiction requires specific
phrasing. Hiding it entirely (disclaimer={null}) opts out of
the regulatory-friendly default — do so only if you display
equivalent copy elsewhere on the page.
now
Active-wave filtering depends on the current wall clock. The
default (() => new Date()) ticks with real time. For Storybook
scenarios or unit tests, pass a frozen function so the aggregation
is deterministic:
<EstimateBreakdown now={() => new Date("2026-07-01T12:00:00Z")} />
children (function form)
Receives the live SelectionEstimate and returns custom rendering.
Use when the default tree doesn't fit — e.g. a side-by-side
comparison table or an inline summary line.
<EstimateBreakdown>
{(estimate) => (
<p>
{estimate.ticketTypes.length} ticket type
{estimate.ticketTypes.length === 1 ? "" : "s"} ·{" "}
<strong>{formatCurrency(estimate.subtotal)}</strong>
</p>
)}
</EstimateBreakdown>
Render output
Default rendering (sketch):
General admission
1× Early bird $80.00
1× Regular $100.00
Subtotal $180.00
VIP
2× Standard $400.00
─────────────────────────────────
Total $580.00
We pick the cheapest available tier first. Final price
(including any fees) is confirmed when you reserve.
The component wraps in <div class="rift-estimate-breakdown"> (or
the slot root). The internal tree uses
.rift-breakdown-tree__* classes shared with
<ReservationSummary> so consumers can style both with one CSS
ruleset.
Behavior
- Data source. Reads
useSelectionEstimate(), which composesuseTicketTypes()and the event-scoped selection from<RiftEvent>. - Empty state. Renders
emptyFallback(default: theestimate.emptylocale string) when no quantity is selected. - Active-wave filtering.
useTicketTypesfilters waves bygetNow()againststartsAt/endsAt— expired and not-yet-live waves never contribute to the displayed price. - Greedy-fill correctness. When a ticket type's quantity
exceeds the cheapest wave's remaining, the breakdown splits:
one line per wave consumed, each at its own
unitPrice. This mirrors the reservation service's fulfillment. - Line-item only. Modifier steps (server-side fees, taxes)
aren't included — the disclaimer covers it. Post-reservation,
<PriceChangedWarning>surfaces the delta if the authoritative total differs. - No layout opinions. No
position, no sticky behavior. The consumer's container decides placement.
Examples
Inline panel next to the picker
<div className="grid grid-cols-[1fr_320px] gap-6">
<AvailabilityList />
<aside>
<EstimateBreakdown />
<CaptchaWidget />
<ReserveButton requireCaptcha />
</aside>
</div>
Sticky bottom drawer (consumer-owned layout)
<div className="pb-32">
<AvailabilityList />
</div>
<div className="fixed inset-x-0 bottom-0 bg-surface border-t p-4">
<EstimateBreakdown />
<ReserveButton requireCaptcha />
</div>
The component is unopinionated — the sticky behavior is the consumer's CSS, not the SDK's.
Custom empty state
<EstimateBreakdown
emptyFallback={
<div className="text-muted">Add tickets to see your breakdown.</div>
}
/>
Override disclaimer per jurisdiction
<EstimateBreakdown disclaimer="IVA y comisiones se confirman al pagar." />
Frozen clock in Storybook
<EstimateBreakdown now={() => new Date("2026-07-01T12:00:00Z")} />
Related
useSelectionEstimate— the underlying hook; exported from@feelrift/react.useTicketTypes— the aggregated view this builds on.<ReservationSummary>— the post-reservation counterpart; shares the same render primitive so the visual contract matches.- Pricing and estimates — walks the wave-tier model, the greedy-fill algorithm, and the disclaimer requirement.