# Order Summary

> Cart and checkout pieces: CartLine rows, the OrderSummary with discount code and free-shipping progress, and CheckoutSteps.

Component · Commerce · Pro kit · [Live page](https://ui.twentyfirst.media/pro/components/OrderSummary/)

Cart and checkout pieces: `CartLine` rows, the `OrderSummary` with discount code and free-shipping progress, and `CheckoutSteps`.

CartLine props: `name`, `variant`, `note`, `art` + `tint`, `price`, `qty`, `onQty(n)`, `onRemove` (the − button becomes a bin at 1).
OrderSummary props: `lines` ({name, variant, art, tint, price, qty}), `showLines`, `discount` ({code, pct | amount, label}), `onCoupon(code)` (return false to show "isn’t valid"; null removes), `shipping` (fixed) or `freeShippingFrom` (default 75, else €4.90), `vat` (19, prices include it), `title`, `action` (the CTA).
CheckoutSteps props: `steps` (Cart, Details, Shipping, Payment), `current`, `onPick` (go back only).

- Keep the summary in a sticky side column on desktop; it moves under the form on phones.
- Show what's left for free shipping; it is the strongest nudge in a cart.
- Removing a line shows an Undo toast instead of a confirm.

## Example

```tsx
import * as React from "react";
import { Button, CartLine, CheckoutSteps, OrderSummary } from "@twentyfirstmedia/pro";
import type { CartLineProps, Discount } from "@twentyfirstmedia/pro";

type OrderLine = Omit<CartLineProps, "onQty" | "onRemove"> & { key: string };

function App() {
  const lines = React.useState<OrderLine[]>([
      {
        key: "a",
        name: "Halo Cotton Tee",
        variant: "M · Sky",
        art: "tee",
        tint: "sky",
        price: 32,
        qty: 1,
      },
      {
        key: "b",
        name: "Studio Mug",
        art: "mug",
        tint: "butter",
        price: 18,
        qty: 2,
        note: "Only 6 left",
      },
    ]),
    d = React.useState<Discount | null>(null),
    step = React.useState(1);
  return (
    <div
      className="tf-col"
      style={{
        gap: 16,
      }}
    >
      <CheckoutSteps current={step[0]} onPick={step[1]} />
      <div
        className="tf-grid"
        style={
          {
            "--cols": "minmax(0, 1fr) 340px",
            gap: 16,
            alignItems: "start",
          } as React.CSSProperties
        }
      >
        <div className="tf-panel">
          {lines[0].map((l) => (
            <CartLine
              {...l}
              key={l.key}
              onQty={(n) => {
                lines[1](
                  lines[0].map((z) =>
                    z.key === l.key
                      ? Object.assign({}, z, {
                          qty: n,
                        })
                      : z,
                  ),
                );
              }}
              onRemove={() => {
                lines[1](lines[0].filter((z) => z.key !== l.key));
              }}
            />
          ))}
        </div>
        <div className="tf-panel">
          <OrderSummary
            lines={lines[0]}
            showLines
            discount={d[0]}
            freeShippingFrom={75}
            onCoupon={(c) => {
              if (c === null) return d[1](null);
              if (c !== "HALO10") return false;
              d[1]({
                code: "HALO10",
                pct: 10,
                label: "10% off everything",
              });
            }}
            action={<Button icon="lock">Pay now</Button>}
          />
        </div>
      </div>
    </div>
  );
}
export default App;
```

## Props

### Button

| Prop | Type | Notes |
| --- | --- | --- |
| `variant?` | `"primary" \| "secondary" \| "soft" \| "ghost" \| "danger" \| "danger-solid"` |  |
| `size?` | `"md" \| "sm"` |  |
| `icon?` | `IconName` |  |
| `iconRight?` | `IconName` |  |
| `disabled?` | `boolean` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `asChild?` | `boolean` | Render the single child element (a link, a router Link) with the button's look instead of a <button>. |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### CartLine

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `variant?` | `string` |  |
| `note?` | `string` |  |
| `art` | `ArtKind` | Required |
| `tint?` | `Tint` |  |
| `price` | `number` | Required |
| `qty` | `number` | Required |
| `onQty?` | `(n: number) => void` |  |
| `onRemove?` | `() => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### CheckoutSteps

| Prop | Type | Notes |
| --- | --- | --- |
| `steps?` | `string[]` |  |
| `current?` | `number` |  |
| `onPick?` | `(i: number) => void` |  |
| `compact?` | `boolean` | Numbers only, with the label of the current step (screen readers still hear every label). Narrow containers (up to 560px) get this on their own. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### OrderSummary

| Prop | Type | Notes |
| --- | --- | --- |
| `lines` | `{ name: string; variant?: string; art: ArtKind; tint?: Tint; price: number; qty: number; }[]` | Required |
| `showLines?` | `boolean` |  |
| `discount?` | `Discount \| null` |  |
| `onCoupon?` | `(code: string \| null) => boolean \| void` |  |
| `shipping?` | `number \| null` |  |
| `freeShippingFrom?` | `number` |  |
| `vat?` | `number` |  |
| `title?` | `string \| false` |  |
| `action?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
