Single-Page Checkout
Contact, shipping and payment in one scrolling form with a single place-order button.
3 frameworksIntermediate
A checkout form beside a sticky order summary that stacks below the form on mobile.
<!--
Form left, summary right - but only from lg up. Below that the grid is one
column and the summary sits *under* the form: on a phone you fill the fields
first and confirm the total last, which is the reading order you want anyway.
Money is text-right + tabular-nums so the price column aligns on the decimal.
-->
<form class="mx-auto grid w-full max-w-5xl gap-6 lg:grid-cols-[1fr_20rem]" action="#" method="post" novalidate>
<div class="space-y-4 rounded-2xl border border-gray-200 bg-white p-5 sm:p-6 dark:border-gray-800 dark:bg-gray-950">
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Shipping & payment</h2>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="ct-email">Email</label>
<input class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950" id="ct-email" name="email" type="email" autocomplete="email" placeholder="you@example.com" required />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="ct-name">Full name</label>
<input class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950" id="ct-name" name="name" type="text" autocomplete="name" placeholder="Ada Lovelace" required />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="ct-card">Card number</label>
<input class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-950" id="ct-card" name="cc-number" type="text" autocomplete="cc-number" inputmode="numeric" placeholder="1234 1234 1234 1234" required />
</div>
</div>
<aside class="h-fit space-y-3 rounded-2xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-800 dark:bg-gray-900">
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Order summary</h2>
<ul class="space-y-2">
<li class="flex items-baseline justify-between gap-3 text-sm">
<span class="min-w-0 text-gray-700 dark:text-gray-300">Wireless keyboard</span>
<span class="shrink-0 text-right tabular-nums text-gray-900 dark:text-gray-100">$79.00</span>
</li>
<li class="flex items-baseline justify-between gap-3 text-sm">
<span class="min-w-0 text-gray-700 dark:text-gray-300">USB-C cable</span>
<span class="shrink-0 text-right tabular-nums text-gray-900 dark:text-gray-100">$12.00</span>
</li>
</ul>
<dl class="space-y-1 border-t border-gray-200 pt-3 text-sm dark:border-gray-800">
<div class="flex justify-between gap-3">
<dt class="text-gray-600 dark:text-gray-400">Subtotal</dt>
<dd class="text-right tabular-nums text-gray-900 dark:text-gray-100">$91.00</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-gray-600 dark:text-gray-400">Shipping</dt>
<dd class="text-right tabular-nums text-gray-900 dark:text-gray-100">$5.00</dd>
</div>
<div class="flex justify-between gap-3 border-t border-gray-200 pt-2 text-base font-semibold dark:border-gray-800">
<dt class="text-gray-900 dark:text-gray-100">Total</dt>
<dd class="text-right tabular-nums text-gray-900 dark:text-gray-100">$96.00</dd>
</div>
</dl>
<button class="inline-flex w-full items-center justify-center rounded-lg bg-blue-600 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:focus-visible:ring-blue-400 dark:focus-visible:ring-offset-gray-900" type="submit">Pay $96.00</button>
</aside>
</form>| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | OrderItem[] | - | The array of items to render. |
currency | string | '$' | Currency |
shipping | number | 0 | Shipping |
onSubmit | (e: FormEvent) => void | - | Called with the form's values when it is submitted. |
The grid is `lg:grid-cols-[1fr_20rem]`; below `lg` it collapses to one column with the summary under the form, so a phone fills fields first and confirms the total last. Every amount is `text-right tabular-nums` so the price column aligns on the decimal. Drive the totals with `items`, `shipping` and `currency`.