Plan picker
Three plans as cards you select, with a monthly or yearly switch that changes the prices and one plan marked as the common choice.
AUTUMN15 · 15% off
Charged to the card ending 2861. Seats renew on Nov 1.
It is live: hover, focus and its own controls respond. Actions that would change data (approve, send, delete) are for your app to wire. The width is a real 390 px, 768 px or 1280 px box, shrunk to fit your screen.
<style>
.odl-order-summary { box-sizing: border-box; inline-size: 100%; max-inline-size: 440px; container-type: inline-size; text-wrap: pretty; }
.odl-order-summary *, .odl-order-summary *::before { box-sizing: border-box; }
.odl-order-summary .odl-card { display: grid; gap: var(--ds-space-stack-lg); padding: var(--ds-space-inset-lg); background: var(--ds-color-surface-raised); color: var(--ds-color-text-primary);
border: var(--ds-border-width-default) solid var(--ds-color-border-subtle); border-radius: var(--ds-radius-container); box-shadow: var(--ds-elevation-raised); font: var(--ds-text-body-md); }
.odl-order-summary .odl-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--ds-space-inline-sm); }
.odl-order-summary h3 { margin: 0; font: var(--ds-text-title-sm); }
.odl-order-summary .odl-head span { color: var(--ds-color-text-secondary); font: var(--ds-text-body-sm); }
.odl-order-summary ul { margin: 0; padding: 0; list-style: none; display: grid; }
.odl-order-summary .odl-item { display: grid; grid-template-columns: var(--ds-size-control-md) minmax(0, 1fr) auto; column-gap: var(--ds-space-inline-sm); row-gap: var(--ds-space-stack-xs);
padding-block: var(--ds-space-stack-sm); }
.odl-order-summary .odl-item + .odl-item { border-block-start: var(--ds-border-width-default) solid var(--ds-color-border-subtle); }
.odl-order-summary .odl-item:first-child { padding-block-start: 0; }
.odl-order-summary .odl-thumb { grid-row: span 2; display: grid; place-items: center; inline-size: var(--ds-size-control-md); block-size: var(--ds-size-control-md);
border-radius: var(--ds-radius-nested); background: var(--ds-color-bg-accent-subtle); color: var(--ds-color-icon-accent); }
.odl-order-summary .odl-thumb svg { inline-size: var(--ds-size-icon-md); block-size: var(--ds-size-icon-md); }
.odl-order-summary .odl-name { display: grid; min-inline-size: 0; }
.odl-order-summary .odl-name b { font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-heading); }
.odl-order-summary .odl-name small { color: var(--ds-color-text-secondary); font: var(--ds-text-body-sm); font-variant-numeric: tabular-nums; }
.odl-order-summary .odl-line { font-variant-numeric: tabular-nums; text-align: end; }
.odl-order-summary .odl-step { grid-column: 2 / -1; justify-self: start; display: inline-flex; align-items: center; border: var(--ds-border-width-default) solid var(--ds-color-border-default);
border-radius: var(--ds-radius-control); background: var(--ds-color-surface-raised); }
.odl-order-summary .odl-step button { display: grid; place-items: center; min-inline-size: var(--ds-size-target-min); min-block-size: var(--ds-size-target-min); padding: 0;
border: 0; border-radius: var(--ds-radius-control); background: transparent; color: var(--ds-color-text-primary); cursor: pointer;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-order-summary .odl-step button:hover:not(:disabled) { background: var(--ds-color-bg-neutral-subtle); }
.odl-order-summary .odl-step button:disabled { color: var(--ds-color-text-disabled); cursor: not-allowed; }
.odl-order-summary .odl-step svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); }
.odl-order-summary .odl-step output { min-inline-size: 3ch; text-align: center; font: var(--ds-text-label-lg); font-variant-numeric: tabular-nums; }
.odl-order-summary .odl-promo { display: grid; gap: var(--ds-space-stack-xs); }
.odl-order-summary .odl-lab { color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); }
.odl-order-summary .odl-field { display: flex; gap: var(--ds-space-inline-xs); }
.odl-order-summary input { flex: 1; min-inline-size: 0; min-block-size: max(var(--ds-size-control-md), var(--ds-size-target-min)); padding: 0 var(--ds-space-inset-sm);
border: var(--ds-border-width-default) solid var(--ds-color-border-input); border-radius: var(--ds-radius-control); background: var(--ds-color-bg-field); color: var(--ds-color-text-primary);
font: var(--ds-text-body-md); text-transform: uppercase; }
.odl-order-summary input::placeholder { color: var(--ds-color-text-tertiary); text-transform: none; }
.odl-order-summary input[aria-invalid="true"] { border-color: var(--ds-color-border-danger); }
.odl-order-summary .odl-apply { min-block-size: max(var(--ds-size-control-md), var(--ds-size-target-min)); padding: 0 var(--ds-space-inset-md); border: var(--ds-border-width-default) solid var(--ds-color-border-default);
border-radius: var(--ds-radius-control); background: var(--ds-color-surface-raised); color: var(--ds-color-text-primary); font: var(--ds-text-label-lg); cursor: pointer;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-order-summary .odl-apply:hover { background: var(--ds-color-bg-neutral-subtle); }
.odl-order-summary .odl-msg { margin: 0; color: var(--ds-color-text-danger); font: var(--ds-text-body-sm); }
.odl-order-summary .odl-msg:empty { display: none; }
.odl-order-summary .odl-chip { justify-self: start; display: inline-flex; align-items: center; gap: var(--ds-space-inline-xs); padding-inline-start: var(--ds-space-inset-sm);
border-radius: var(--ds-radius-control-sm); background: var(--ds-color-bg-success-subtle); color: var(--ds-color-text-success); font: var(--ds-text-label-md); }
.odl-order-summary .odl-chip[hidden] { display: none; }
.odl-order-summary .odl-chip code { font: var(--ds-text-code-sm); font-weight: var(--ds-font-weight-label); }
.odl-order-summary .odl-chip svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); }
.odl-order-summary .odl-chip button { display: grid; place-items: center; min-inline-size: var(--ds-size-target-min); min-block-size: var(--ds-size-target-min); padding: 0; margin-block: calc(-1 * var(--ds-space-8));
border: 0; border-radius: var(--ds-radius-full); background: transparent; color: inherit; cursor: pointer; }
.odl-order-summary .odl-chip button:hover { color: var(--ds-color-text-primary); }
.odl-order-summary dl { margin: 0; display: grid; gap: var(--ds-space-stack-xs); padding-block-start: var(--ds-space-stack-md); border-block-start: var(--ds-border-width-default) solid var(--ds-color-border-subtle);
font-variant-numeric: tabular-nums; }
.odl-order-summary dl div { display: flex; justify-content: space-between; gap: var(--ds-space-inline-md); }
.odl-order-summary dt { color: var(--ds-color-text-secondary); }
.odl-order-summary dd { margin: 0; text-align: end; }
.odl-order-summary .odl-off dd { color: var(--ds-color-text-success); }
.odl-order-summary .odl-off[hidden] { display: none; }
.odl-order-summary .odl-total { align-items: baseline; margin-block-start: var(--ds-space-stack-xs); padding-block-start: var(--ds-space-stack-sm); border-block-start: var(--ds-border-width-default) solid var(--ds-color-border-subtle); }
.odl-order-summary .odl-total dt { color: var(--ds-color-text-primary); font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-heading); }
.odl-order-summary .odl-total dd { font: var(--ds-text-numeric-md); font-weight: var(--ds-font-weight-heading); }
.odl-order-summary .odl-total dd small { margin-inline-start: var(--ds-space-4); color: var(--ds-color-text-tertiary); font: var(--ds-text-label-sm); }
.odl-order-summary .odl-pay { display: inline-flex; align-items: center; justify-content: center; gap: var(--ds-space-inline-xs); inline-size: 100%;
min-block-size: max(var(--ds-size-control-lg), var(--ds-size-target-min)); padding: 0 var(--ds-space-inset-md); border: 0; border-radius: var(--ds-radius-control);
background: var(--ds-color-bg-action-primary); color: var(--ds-color-text-on-action); font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-heading); font-variant-numeric: tabular-nums; cursor: pointer;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-order-summary .odl-pay:hover { background: var(--ds-color-bg-action-primary-hover); }
.odl-order-summary .odl-pay:active { background: var(--ds-color-bg-action-primary-pressed); }
.odl-order-summary .odl-pay svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); }
.odl-order-summary .odl-fine { margin: calc(-1 * var(--ds-space-stack-sm)) 0 0; color: var(--ds-color-text-secondary); font: var(--ds-text-body-sm); text-align: center; text-wrap: balance; }
.odl-order-summary button:focus-visible, .odl-order-summary input:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
@container (max-width: 22rem) {
.odl-order-summary .odl-card { padding: var(--ds-space-inset-md); }
.odl-order-summary .odl-item { grid-template-columns: minmax(0, 1fr) auto; }
.odl-order-summary .odl-thumb { display: none; }
.odl-order-summary .odl-step { grid-column: 1 / -1; }
}
@media (prefers-reduced-motion: reduce) { .odl-order-summary, .odl-order-summary *, .odl-order-summary *::before { transition: none !important; animation: none !important; } }
</style>
<section class="odl-order-summary" aria-label="Order summary">
<div class="odl-card">
<div class="odl-head"><h3>Order summary</h3><span>Oskarby Analytics</span></div>
<ul>
<li class="odl-item" data-price="48" data-unit="seat" data-units="seats">
<span class="odl-thumb" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="7.5" cy="7" r="2.75"/><path d="M2.5 16c.6-2.6 2.6-4 5-4s4.4 1.4 5 4"/><path d="M13 4.5a2.5 2.5 0 0 1 0 5M15 12.4c1.3.6 2.2 1.8 2.5 3.6"/></svg></span>
<span class="odl-name"><b>Extra seats</b><small>$48.00 per seat a month</small></span>
<span class="odl-line">$96.00</span>
<div class="odl-step" role="group" aria-label="Extra seats quantity">
<button type="button" data-d="-1" aria-label="One seat fewer"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M3.5 8h9"/></svg></button>
<output aria-live="polite">2</output>
<button type="button" data-d="1" aria-label="One seat more"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M3.5 8h9M8 3.5v9"/></svg></button>
</div>
</li>
<li class="odl-item" data-price="250" data-unit="pack" data-units="packs">
<span class="odl-thumb" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M11 2.5 4.5 11h5l-1 6.5L15.5 9h-5z"/></svg></span>
<span class="odl-name"><b>Usage credit, $250 pack</b><small>Used before card charges; never expires</small></span>
<span class="odl-line">$500.00</span>
<div class="odl-step" role="group" aria-label="Usage credit quantity">
<button type="button" data-d="-1" aria-label="One pack fewer"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M3.5 8h9"/></svg></button>
<output aria-live="polite">2</output>
<button type="button" data-d="1" aria-label="One pack more"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M3.5 8h9M8 3.5v9"/></svg></button>
</div>
</li>
</ul>
<div class="odl-promo">
<span class="odl-lab">Promo code</span>
<div class="odl-field">
<input type="text" aria-label="Promo code" autocomplete="off" spellcheck="false" placeholder="Enter a code">
<button type="button" class="odl-apply">Apply</button>
</div>
<p class="odl-msg" role="alert"></p>
<span class="odl-chip"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true"><path d="M8.6 1.75h5.65V7.4L7.4 14.25 1.75 8.6z"/><circle cx="11" cy="5" r=".9"/></svg>
<span><code>AUTUMN15</code> · 15% off</span>
<button type="button" class="odl-remove" aria-label="Remove code AUTUMN15"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m4.5 4.5 7 7M11.5 4.5l-7 7"/></svg></button></span>
</div>
<dl>
<div><dt>Subtotal</dt><dd class="odl-sub">$596.00</dd></div>
<div class="odl-off"><dt>Discount, 15%</dt><dd class="odl-disc">−$89.40</dd></div>
<div><dt>Sales tax, 8.25%</dt><dd class="odl-tax">$41.79</dd></div>
<div class="odl-total"><dt>Due today</dt><dd><span class="odl-sum">$548.39</span><small>USD</small></dd></div>
</dl>
<button type="button" class="odl-pay"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="3" y="7" width="10" height="7" rx="1.5"/><path d="M5.25 7V5a2.75 2.75 0 0 1 5.5 0v2"/></svg>Pay <span class="odl-paysum">$548.39</span></button>
<p class="odl-fine">Charged to the card ending 2861. Seats renew on Nov 1.</p>
</div>
</section>
<script>
document.querySelectorAll(".odl-order-summary").forEach((root) => {
if (root.dataset.odlReady) return; // wire once, even if the file is pasted twice
root.dataset.odlReady = "1";
const CODE = "AUTUMN15", RATE = 0.15, TAX = 0.0825;
const money = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const $ = (s) => root.querySelector(s);
const input = $("input"), msg = $(".odl-msg"), chip = $(".odl-chip");
let applied = true;
const update = () => {
let sub = 0;
root.querySelectorAll(".odl-item").forEach((item) => {
const q = Number(item.querySelector("output").textContent), line = q * Number(item.dataset.price);
sub += line;
item.querySelector(".odl-line").textContent = money(line);
item.querySelector('[data-d="-1"]').disabled = q <= 1;
item.querySelector('[data-d="1"]').disabled = q >= 20;
});
const disc = applied ? Math.round(sub * RATE * 100) / 100 : 0;
const tax = Math.round((sub - disc) * TAX * 100) / 100, total = sub - disc + tax;
$(".odl-sub").textContent = money(sub);
$(".odl-disc").textContent = "−" + money(disc);
$(".odl-off").hidden = !applied;
chip.hidden = !applied;
$(".odl-tax").textContent = money(tax);
$(".odl-sum").textContent = money(total);
$(".odl-paysum").textContent = money(total);
};
root.querySelectorAll(".odl-step button").forEach((b) => b.addEventListener("click", () => {
const out = b.parentElement.querySelector("output");
out.textContent = Math.min(20, Math.max(1, Number(out.textContent) + Number(b.dataset.d)));
update();
}));
$(".odl-remove").addEventListener("click", () => { applied = false; update(); input.focus(); });
const apply = () => {
const code = input.value.trim().toUpperCase();
if (!code) return;
const ok = code === CODE;
msg.textContent = ok ? "" : `${code} isn't a valid code. Check it and try again.`;
input.setAttribute("aria-invalid", String(!ok));
if (ok) { applied = true; input.value = ""; update(); }
};
$(".odl-apply").addEventListener("click", apply);
input.addEventListener("keydown", (e) => { if (e.key === "Enter") apply(); });
update();
});
</script>
Three plans as cards you select, with a monthly or yearly switch that changes the prices and one plan marked as the common choice.
A card for a digital product or plan add-on: a cover drawn from the tokens, name, one-line description, rating, price with the old price struck out, an Add button and a save toggle.
Four headline numbers in one band, each with a plain note under it, and a small bar where a share matters.