익스클루시브
All-in Price <mv-all-in>
드립 프라이싱이 숨기는 것을 분명히 말해 주는, 정직한 총액 가격 표시.
문화적 레퍼런스
벌거벗은 임금님, 한스 크리스티안 안데르센 (1837, 우화). 모두가 존재하지도 않는 옷을 칭찬하는 척하다가, 한 아이가 모두의 눈에 보이는 것을 그대로 말합니다. UI는 그 아이의 역할을 합니다: 모두가 실제로 지불할 금액이 아니라는 걸 아는 광고 가격에 맞장구치는 대신, 실제 총액과 모든 숨은 비용을 가격 옆에 쉬운 말로 밝힙니다.
작동 방식
드립 프라이싱이 숨기는 것을 분명히 말해 주는, 정직한 총액 가격 표시. 어떤 가격이든(상품 카드, 호텔이나 렌털 예약, 티켓 판매, SaaS 결제) 감싸고, 광고 기본 가격과 모든 요금을 data-fee 자식 요소 또는 lines 속성으로 선언합니다: 고정 또는 비율 수수료, 단위당 또는 주문당 요금, 지역별 세금(기본 가격, 소계 또는 총액 기준), 배송비, 카드 할증료, 할인, 일회성 설치비, 나중에 지불할 금액(“at the property”). 헤드라인은 항상 고객이 실제로 지불할 총액(“$713.05 total for 3 nights”)이며, 광고 가격은 보조로 표시됩니다(“Room rate $164/night · + $221.05 fees & taxes”). “What’s included · 4 charges” 펼침 영역은 실제 표에서 모든 항목을 쉬운 설명, 지금 지불과 나중에 지불 행, 갱신 가격과 함께 나열하며, 선택 추가 항목은 별도의 체크리스트에 따로 두고 절대 미리 추가하지 않습니다. 숨은 비용 패턴은 작은 글씨가 아니라 가격 바로 옆에 표시됩니다: 첫 가격보다 높은 갱신 가격(“Then $62.03/mo after 3 months”), 최소 수량이 있는 단위 가격(“Minimum 3 seats: priced for 3”), 연간 청구 금액, “from” 가격, 나중에 지불하는 수수료. 금액은 통화를 고려한 반올림과 함께 Intl.NumberFormat을 사용합니다(기본값 en-US / USD, 모든 로케일과 통화 지원). 함께 제공되는 <mv-all-in-list>는 비교 목록을 공정하게 만듭니다: 모든 카드가 총액을 표시하고, 항목은 총액 또는 광고 가격 순으로 재정렬되며(FLIP, 포커스 유지) 가장 낮은 총액이 표시됩니다. 2024-2025년의 총액 가격 표시 추세(정크 수수료 및 드립 프라이싱 규제)를 염두에 두고 만들었지만, 앱이 선언한 내용을 표시할 뿐 그 자체로 결제 과정을 규정에 맞게 만들어 주지는 않습니다.
| 카테고리 | 데이터 표시 |
|---|---|
| 유형 | Web Component (<mv-all-in>) |
| 상태 | 안정 |
| 키트 | 신뢰와 개인정보 보호 |
| Keywords | exclusive, culture, price, pricing, total-price, all-in-pricing, fees, junk-fees, drip-pricing, taxes, vat, breakdown, checkout, booking, ticketing, subscription, renewal, e-commerce, comparison, currency, intl |
When to use
- A booking, ticketing or rental page must lead with the total including every mandatory fee and tax, not the nightly or per-ticket rate
- A subscription checkout has an intro price, a higher renewal, a per-seat minimum or annual billing that must be stated next to the price
- Offers from several sellers must be compared and sorted by what the customer will actually pay
- Taxes and currency depend on the buyer's region and optional add-ons must stay out of the total until the customer adds them
Avoid when
- Only a formatted price with a compare-at discount is needed and there are no fees or taxes to disclose → use Price instead
- Several subscription plans must be compared feature by feature with a monthly / yearly switch → use Pricing instead
- The shopper is editing a multi-item cart with promo codes, shipping choice and a checkout button → use Cart instead
설치
node scripts/add.mjs all-in --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["all-in"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/all-in/all-in.js, components/all-in/all-in.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-all-in base="164" quantity="3" unit="night">
<span data-fee="32" data-per="unit">Destination fee</span>
<span data-fee data-kind="tax" data-rate="17.49%">Hotel tax</span>
</mv-all-in>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="ai-demo" style="width:min(100%,64rem);margin-inline:auto">
<style>
#ai-demo { display:grid; gap:1.25rem; align-content:start }
#ai-demo .ai-top { display:grid; grid-template-columns:minmax(0,1.25fr) minmax(0,1fr); gap:1.25rem; align-items:start }
#ai-demo .ai-card { display:grid; gap:1rem; min-width:0; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#ai-demo .ai-eyebrow { margin:0; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
#ai-demo .ai-title { margin:.125rem 0 0; font-size:1.0625rem; font-weight:650; letter-spacing:-.01em }
#ai-demo .ai-meta { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#ai-demo .ai-media { display:flex; gap:.875rem; align-items:flex-start }
#ai-demo .ai-thumb { flex:none; display:grid; place-items:center; width:4.5rem; height:4.5rem; border-radius:var(--mv-radius-lg); background:linear-gradient(135deg, color-mix(in oklab, var(--mv-accent) 22%, var(--mv-bg-muted)), var(--mv-bg-muted)); color:var(--mv-fg-muted) }
#ai-demo .ai-thumb svg { width:1.75rem; height:1.75rem }
#ai-demo .ai-controls { display:flex; flex-wrap:wrap; align-items:center; gap:.625rem 1rem; padding-top:1rem; border-top:1px solid var(--mv-border) }
#ai-demo .ai-control { display:grid; gap:.3125rem }
#ai-demo .ai-label { color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
#ai-demo .ai-log { min-height:1.1rem; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
#ai-demo .ai-compare { display:grid; gap:1rem; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#ai-demo .ai-compare-head { display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:.75rem 1.25rem }
#ai-demo .ai-compare-tools { display:flex; flex-wrap:wrap; gap:.625rem 1rem }
#ai-demo .ai-offer { display:grid; grid-template-columns:minmax(0,13rem) minmax(0,1fr) auto; gap:.75rem 1.25rem; align-items:start; padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface) }
#ai-demo .ai-seller { display:grid; gap:.25rem; min-width:0 }
#ai-demo .ai-seller h4 { margin:0; font-size:.9375rem; font-weight:650 }
#ai-demo .ai-seller p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.45 }
#ai-demo .ai-offer .mv-button { align-self:start }
@media (max-width:52rem) {
#ai-demo .ai-top { grid-template-columns:minmax(0,1fr) }
#ai-demo .ai-offer { grid-template-columns:minmax(0,1fr) }
}
</style>
<div class="ai-top">
<!-- Hotel booking: the total leads, the nightly rate is secondary, the destination fee is due at the property. -->
<section class="ai-card" aria-labelledby="ai-hotel-title">
<div class="ai-media">
<span class="ai-thumb" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V8l9-5 9 5v13"/><path d="M9 21v-6h6v6"/><path d="M8 10h.01M12 10h.01M16 10h.01"/></svg></span>
<div>
<p class="ai-eyebrow">Hotel · Chicago, IL</p>
<h3 class="ai-title" id="ai-hotel-title">The Wrenfield River North</h3>
<p class="ai-meta">Deluxe King · Wed, Oct 14 to Sat, Oct 17, 2026 · 2 guests</p>
</div>
</div>
<mv-all-in id="ai-hotel" base="164" quantity="3" unit="night" base-label="Room rate" data-size="lg">
<span data-fee="32" data-per="unit" data-due="at the property" data-note="Wi-Fi, fitness center and two welcome drinks. Charged by the hotel.">Destination fee</span>
<span data-fee="18.90" data-note="Charged by the booking site to process your reservation.">Service fee</span>
<span data-fee data-kind="tax" data-rate="17.49%" data-note="State, county and city hotel taxes.">Chicago hotel tax</span>
<span data-fee="28" data-per="unit" data-optional data-note="Served 7-10:30 a.m. in the lobby café.">Breakfast for two</span>
<span data-fee="72" data-per="unit" data-optional data-note="In-and-out privileges.">Valet parking</span>
<span data-fee="40" data-optional data-note="Subject to availability, confirmed at check-in.">Late checkout (2 p.m.)</span>
</mv-all-in>
<div class="ai-controls">
<div class="ai-control">
<span class="ai-label" id="ai-nights-l">Nights</span>
<mv-segmented id="ai-nights" aria-labelledby="ai-nights-l" name="nights" value="3">
<button value="2">2</button>
<button value="3">3</button>
<button value="5">5</button>
</mv-segmented>
</div>
<div class="ai-control">
<span class="ai-label" id="ai-head-l">Headline</span>
<mv-segmented id="ai-head" aria-labelledby="ai-head-l" name="headline" value="total">
<button value="total">All-in total</button>
<button value="base">Base price</button>
</mv-segmented>
</div>
<p class="ai-log" id="ai-log" aria-live="polite" style="flex:1 1 12rem;align-self:end">Open “What’s included” or add an extra.</p>
</div>
</section>
<!-- SaaS checkout: intro price, higher renewal, per-seat minimum, tax and currency by region. -->
<section class="ai-card" aria-labelledby="ai-saas-title">
<div>
<p class="ai-eyebrow">Checkout · Team plan</p>
<h3 class="ai-title" id="ai-saas-title">Pipewren CRM</h3>
<p class="ai-meta">Shared inbox, pipelines and reporting for small sales teams.</p>
</div>
<mv-all-in id="ai-saas" base="12" renewal="19" intro-periods="3" unit="seat" period="month" quantity="2" min-quantity="3" region="US-NY" base-label="Intro price">
<span data-fee data-kind="tax" data-rate="8.875" data-region="US-NY">New York sales tax</span>
<span data-fee data-kind="tax" data-rate="19" data-region="DE">German VAT</span>
<span data-fee data-kind="tax" data-rate="20" data-region="GB">UK VAT</span>
<span data-fee="2" data-per="unit" data-optional data-note="10 GB extra per seat.">Extra file storage</span>
</mv-all-in>
<div class="ai-controls">
<div class="ai-control">
<span class="ai-label" id="ai-seats-l">Seats</span>
<mv-segmented id="ai-seats" aria-labelledby="ai-seats-l" name="seats" value="2">
<button value="2">2</button>
<button value="5">5</button>
<button value="12">12</button>
</mv-segmented>
</div>
<div class="ai-control">
<label class="ai-label" for="ai-region">Billing country</label>
<select class="mv-select" id="ai-region" data-size="sm">
<option value="US-NY" selected>United States (NY)</option>
<option value="US-OR">United States (OR)</option>
<option value="DE">Germany</option>
<option value="GB">United Kingdom</option>
</select>
</div>
</div>
</section>
</div>
<!-- Ticket comparison: every offer shows its all-in total, sorted by what you will actually pay. -->
<section class="ai-compare" aria-labelledby="ai-cmp-title">
<div class="ai-compare-head">
<div>
<p class="ai-eyebrow">2 tickets · Floor B, Row 12</p>
<h3 class="ai-title" id="ai-cmp-title">Ashgrove Arena · Sat, Nov 7, 2026, 8:00 p.m.</h3>
</div>
<div class="ai-compare-tools">
<div class="ai-control">
<span class="ai-label" id="ai-show-l">Show</span>
<mv-segmented id="ai-show" aria-labelledby="ai-show-l" name="show" value="total">
<button value="total">All-in totals</button>
<button value="base">Advertised prices</button>
</mv-segmented>
</div>
<div class="ai-control">
<span class="ai-label" id="ai-sort-l">Sort by</span>
<mv-segmented id="ai-sort" aria-labelledby="ai-sort-l" name="sort" value="total">
<button value="total">Total</button>
<button value="base">Advertised</button>
</mv-segmented>
</div>
</div>
</div>
<mv-all-in-list id="ai-list" sort="total" headline="total">
<article class="ai-offer" data-name="StubLoft">
<div class="ai-seller">
<h4>StubLoft</h4>
<p>Resale marketplace · delivered to your phone 48 h before the show</p>
</div>
<mv-all-in base="89" quantity="2" unit="ticket" data-size="sm">
<span data-fee data-rate="27%" data-note="Marketplace fee, charged per order.">Service fee</span>
<span data-fee="7.50" data-per="unit" data-note="Collected for the venue.">Facility fee</span>
<span data-fee="5.95">Order processing</span>
<span data-fee data-kind="tax" data-rate="9%">Amusement tax</span>
</mv-all-in>
<button type="button" class="mv-button" data-variant="outline" data-size="sm">Select</button>
</article>
<article class="ai-offer" data-name="Ticketvale">
<div class="ai-seller">
<h4>Ticketvale</h4>
<p>Verified resale · instant mobile transfer</p>
</div>
<mv-all-in base="104" quantity="2" unit="ticket" data-size="sm">
<span data-fee="11" data-per="unit">Service fee</span>
<span data-fee data-kind="tax" data-rate="9%">Amusement tax</span>
</mv-all-in>
<button type="button" class="mv-button" data-variant="outline" data-size="sm">Select</button>
</article>
<article class="ai-offer" data-name="Ashgrove Arena box office">
<div class="ai-seller">
<h4>Ashgrove Arena box office</h4>
<p>Official seller · pick up at will call or print at home</p>
</div>
<mv-all-in base="118" quantity="2" unit="ticket" data-size="sm">
<span data-fee="3" data-per="unit" data-note="Venue restoration fee.">Facility fee</span>
<span data-fee data-kind="tax" data-rate="9%">Amusement tax</span>
</mv-all-in>
<button type="button" class="mv-button" data-variant="outline" data-size="sm">Select</button>
</article>
</mv-all-in-list>
</section>
<script type="module">
const hotel = document.getElementById("ai-hotel");
const saas = document.getElementById("ai-saas");
const list = document.getElementById("ai-list");
const log = document.getElementById("ai-log");
const say = (text) => { log.textContent = text; };
document.getElementById("ai-nights").addEventListener("mv-change", (e) => { hotel.quantity = Number(e.detail.value); });
document.getElementById("ai-head").addEventListener("mv-change", (e) => { hotel.headline = e.detail.value; });
hotel.addEventListener("mv-breakdown-open", (e) => say(`mv-breakdown-open · total ${hotel.formattedTotal}`));
hotel.addEventListener("mv-extra-change", (e) => say(`mv-extra-change · ${e.detail.label} ${e.detail.added ? "added" : "removed"}`));
document.getElementById("ai-seats").addEventListener("mv-change", (e) => { saas.quantity = Number(e.detail.value); });
// Each billing country has its own currency and list price.
const regions = {
"US-NY": { currency: "USD", locale: "en-US", base: 12, renewal: 19 },
"US-OR": { currency: "USD", locale: "en-US", base: 12, renewal: 19 },
DE: { currency: "EUR", locale: "de-DE", base: 11, renewal: 18 },
GB: { currency: "GBP", locale: "en-GB", base: 10, renewal: 16 },
};
document.getElementById("ai-region").addEventListener("change", (e) => {
const r = regions[e.target.value];
Object.assign(saas, { currency: r.currency, locale: r.locale, base: r.base, renewal: r.renewal, region: e.target.value });
});
document.getElementById("ai-show").addEventListener("mv-change", (e) => { list.headline = e.detail.value; });
document.getElementById("ai-sort").addEventListener("mv-change", (e) => { list.sort = e.detail.value; });
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
base | number | 0 | Advertised base price per unit and per period (the rate drip pricing would show alone). Shown secondary to the total, or as the headline with headline="base". |
quantity | number | 1 | Units bought (nights, tickets, seats). Multiplies the base and every data-per="unit" line. |
min-quantity | number | Minimum billable units (per-seat minimums). The total is computed on max(quantity, min-quantity); a flag says “Minimum 3 seats: priced for 3” when the request is below it, “Minimum 3 seats” otherwise. | |
unit / units | string | Unit name, singular and plural (units defaults to unit + "s"): “$164/night”, “total for 3 nights”, “$89 × 2 tickets”. | |
period | day | night | week | month | year | string | Makes the price recurring: “$12/seat/mo”, headline “$39.19 /mo total”. | |
billed-every | number | 1 | Periods charged at once (12 with period="month" = billed annually). The headline becomes the amount charged (“billed annually”) and a flag gives the per-period equivalent. |
renewal | number | Base price per unit after the intro periods. The renewal total is computed with the same fees, taxes and quantity (one-time lines excluded) and flagged next to the price: “Then $62.03/mo after 3 months” (warning tone when higher), plus a “Then, from month 4” row in the breakdown. | |
intro-periods | number | Number of periods billed at the base (intro) price before renewal applies. 1 reads “after the first year”; 12 months too. | |
from | boolean | The price is a starting price: “From” precedes the headline, the sentence starts with “From” and a flag says options can raise it. | |
region | string | Buyer region code (e.g. US-NY, DE, GB). Lines with data-region apply only when it matches (a line region “US” also matches “US-NY”; several codes separated by spaces or commas; “*” always applies). | |
currency | ISO 4217 code | USD | Currency of every amount; rounding follows its minor units (JPY has none). |
locale | BCP 47 tag | en-US | Number formatting locale. Whole amounts drop the cents ($164), others keep them ($187.40). |
headline | total | base | total | What the big number shows. base keeps the total on the line right under it (“$713.05 total · incl. $221.05 fees & taxes”) and in the accessible sentence. Inherited from an enclosing <mv-all-in-list headline> when not set. |
base-label | string | Base price | Name of the advertised price in the secondary line and the breakdown (“Room rate”, “Intro price”, “Ticket price”). |
open | boolean | Breakdown expanded. Reflects the disclosure state; setting it opens or closes the panel and emits mv-breakdown-open / mv-breakdown-close. | |
data-size | sm | lg | Compact (list rows, cards) or large (booking summary) headline. Default is medium. | |
data-headline / data-lowest | state | Set by the component (styleable): the effective headline mode, and data-lowest on the price with the lowest total inside an <mv-all-in-list>. | |
sort (mv-all-in-list) | none | total | base | none | Reorders the list children by all-in total or by advertised base price, lowest first (stable, original order for ties); none restores the original order. |
headline (mv-all-in-list) | total | base | Headline mode for every contained price that doesn't set its own, so all cards compare on the same number. | |
highlight (mv-all-in-list) | lowest | none | lowest | Marks the price with the lowest total (data-lowest + a “Lowest total” chip) when the list has two items or more. |
Properties
| Name | 유형 | Description |
|---|---|---|
lines | Array<{ id?, label, amount?, rate?, kind?, per?, on?, optional?, once?, due?, note?, region? }> | Declared charges. Setting an array replaces the data-fee children; null goes back to them. Reading returns normalized copies. amount is a fixed amount (discounts may be negative), rate a percentage (8.875 = 8.875%); kind: fee | tax | delivery | surcharge | discount; per: order | unit; on: base | subtotal | total (defaults: tax → subtotal, surcharge → total, others → base). |
extras | string[] | Ids of the optional extras the customer added. Extras start unchecked; set it only to restore a choice the customer already made. |
total | number | All-in amount for the current selection (read-only, rounded to the currency's minor units). |
formattedTotal | string | The total formatted for the locale and currency (read-only). |
summary | object | Read-only computation: { currency, locale, region, quantity, requestedQuantity, cycles, base, baseUnit, fees, taxes, discounts, extras, total, dueNow, dueLater: [{ due, amount }], renewal, lines: [{ id, label, kind, amount, optional, due }] }. |
sentence | string | The accessible sentence used as the price's name (read-only). |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement, with {placeholders} (e.g. total, totalFor, basePrice, plusFeesTaxes, noFees, toggle, toggleCount, groupFees, groupTaxes, dueLater, thenRow, extras, extrasHint, flagRenewal, flagRenewalFirst, flagMinimumApplied, flagBilled, flagDue, flagFrom, lowest, sentence, including, announce, extraAdded; period names and suffixes of the built-in periods: periodDay … periodHour ("month|months", plural form picked for locale), perDay … perHour ("/mo"); sentencePer, sentenceFor, cyclesDetail, unitFallback, unitsFallback, chargeFallback). English defaults. <mv-all-in-list> has its own strings (listSorted, listUnsorted, listTotals, sortTotal, sortBase). |
items (mv-all-in-list) | Element[] | The list's current children in display order (read-only). |
Methods
| Name | Description |
|---|---|
toggle(force?) | Opens or closes the breakdown like the button (opening emits the cancelable mv-breakdown-open). Returns false if the opening was cancelled. |
refresh() | Re-reads the declarations and redraws. Changes to data-fee children and attributes are picked up automatically; this is for bulk edits or custom flows. On <mv-all-in-list>, re-sorts and re-marks after you insert items. |
Events
| Name | Description |
|---|---|
mv-breakdown-open | The breakdown is opening. Cancelable when triggered by the button or toggle(). detail: { total, summary }. |
mv-breakdown-close | The breakdown closed. detail: { total }. |
mv-extra-change | Cancelable, before an optional extra is added or removed by the customer. detail: { id, label, added, amount }. preventDefault() keeps the previous state (e.g. out of stock). |
mv-total-change | The total changed (quantity, region, currency, extras, declarations). detail: { total, previous, summary }. Not emitted on first render. |
mv-sort (mv-all-in-list) | The list was reordered after its sort changed. detail: { by, items }. |
Content structure
| Name | Description |
|---|---|
[data-fee] children | One element per charge, hidden: its text is the label, data-fee the fixed amount (empty with data-rate="8.875%" for a percentage), plus data-kind, data-per="unit", data-on, data-optional, data-once, data-due="at the property", data-note (plain-language explanation), data-region and data-id. |
CSS classes
| Name | Description |
|---|---|
mv-all-in-price | Price block: -lowest (chip), -headline (-from, -amount, -suffix; aria-hidden), -sub (-base, -base-label, -base-value, -delta[data-tone=fees|clear|extras|discount], -total-inline) and the visually hidden -sentence. |
mv-all-in-flags / mv-all-in-flag | List of hidden-cost flags; data-tone="warning" | "info", data-flag="renewal" | "minimum" | "billed" | "due" | "once" | "from". |
mv-all-in-toggle | Disclosure button: -toggle-label, -toggle-count, -chevron. |
mv-all-in-panel | Collapsible panel (data-open, inert while closed) with -panel-inner, the breakdown -table (tbody[data-group=base|fees|taxes|extras|discounts], -group, -item, -item-label, -item-note, -tag, -value, -total-row, -due-row, -then-row). |
mv-all-in-extras | Optional extras fieldset: -extras-legend, -extras-hint, -extras-list, -extra (label), -check, -extra-label, -extra-note, -extra-amount. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-all-in-flag | var(--mv-warning) | Tone of warning flags (renewal higher than intro, minimum applied) and of the renewal row. |
--mv-all-in-good | var(--mv-success) | Tone of “No added fees” and of the “Lowest total” chip. |
Accessibility
The visible price is aria-hidden and replaced for assistive technologies by one complete, visually hidden sentence that always starts with the total, whatever the headline mode: “$713.05 total for 3 nights, including $221.05 in fees and taxes; base price $164 per night” (“From”, extras added, discounts and billing cycle are woven in), so a screen reader never hears a base price without its total. Hidden-cost flags are a real list of plain text next to the price, each with an icon and words (color is never the only signal). “What’s included” is a native <button> with aria-expanded and aria-controls pointing to the panel; the collapsed panel is inert and hidden, Escape inside it collapses it and returns focus to the button. The breakdown is a real <table> with a caption, column headers, row headers per charge, rowgroup headers (Fees, Taxes, Extras you added) and a tfoot with the total, due-now / due-later rows and the renewal row; notes and tags (“Due at the property”, “minimum”) sit inside the row header so they are read with their charge. Optional extras are a <fieldset> with a legend and a hint (“Not included unless you add them.”), native checkboxes wrapped in their <label>, described by their note; they are never pre-checked. Total changes (quantity, region, extras) are announced politely once, debounced (“Breakfast for two added. Total now $843.46.”); inside <mv-all-in-list> the list announces instead (“Totals updated. Lowest: Ticketvale, $250.70.” / “Sorted by total price, lowest first.”) and reordering keeps DOM order equal to visual order and restores focus to the element that had it. Keyboard: Tab reaches the button, the extras and nothing decorative; visible focus rings, forced-colors borders on flags and chips. Reduced motion (OS or data-motion="reduce"): the panel, total and list reorder change without animation. Known limits: it displays and adds up what the app declares (it is not a tax engine and does not make a checkout compliant by itself); without the units attribute the plural of unit is unit + "s", and a custom period (other than day, night, week, month, year, hour) is pluralized the same way, so set units and strings for other languages; each line is rounded to the currency's minor units before summing.