OrderSummary
The money breakdown of a cart or checkout: subtotal, shipping, tax and discounts as a description list, an emphasised total, an optional free-shipping progress bar, and a footer for the checkout button. It is presentational: it prints the figures it is given and adds nothing up, so your cart or your commerce platform stays the source of truth.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
OrderSummary is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { OrderSummary } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
The money breakdown of a cart or checkout: subtotal, shipping, tax and discounts as a description list, an emphasised total, an optional free-shipping progress bar, and a footer for the checkout button. It is presentational: it prints the figures it is given and adds nothing up, so your cart or your commerce platform stays the source of truth.
Use it when
- A cart page or checkout needs the totals beside the items.
- An order confirmation or receipt repeats what was charged.
- A cart drawer shows a compact subtotal with the checkout button (pass fewer
lines).
Don't use it when
- You are comparing plans or prices side by side. Use
Tableor plan cards. - The numbers are not money for one order (analytics, balances). Use
StatorTable. - You need the component to calculate tax, shipping or discounts. Compute them in your app or platform and pass the results.
Example
<OrderSummary
currency="USD"
locale="en-US"
lines={[
{ label: "Subtotal", amount: 138 },
{ label: "Shipping", amount: 0, hint: "Standard, 3โ5 days" },
{ label: "Discount (SUMMER10)", amount: 13.8, kind: "discount" },
{ label: "Tax", amount: 9.94, kind: "muted", hint: "Estimated" },
]}
total={{ amount: 134.14 }}
freeShippingProgress={{ current: 138, threshold: 150 }}
footer={
<>
<PromoCode value={code} onChange={setCode} onApply={apply} />
<Button fullWidth size="lg">Check out</Button>
</>
}
/>
Variants
| Prop | What it is for |
|---|---|
lines[].kind="default" | An ordinary figure: subtotal, shipping, tax. |
lines[].kind="discount" | Money off. The amount always shows as a negative ("-$13.80"), in the discount colour. |
lines[].kind="muted" | An estimate or a figure still to come: "Tax, calculated at next step". Label and figure in tertiary text. |
lines[].hint | A smaller line under the label: the shipping method, "Estimated". |
amount: 0 | Shows "Free" (from Price), e.g. for free shipping. |
total | Emphasised under a rule: label-lg label, Price at size="lg". label defaults to "Total". |
freeShippingProgress | A Progress bar and "You're $12.00 away from free shipping", or "You've got free shipping" (success tone) once current reaches threshold. The remaining amount is the one subtraction the component does, for the message. |
footer | The checkout Button, a PromoCode, payment marks, trust copy. |
title / headingLevel | The heading, "Order summary" by default, as an h2 unless you pick another level to fit the page outline. |
Composition
A panel on its own (it draws its own border and padding). It sits in the side column of a cart or checkout page, under the lines in a Drawer, or on an order confirmation. It renders Price for every figure, Progress for free shipping and Heading for its title; put PromoCode and a Button in footer.
Tokens
Tier 3, in tokens/component/commerce.css, colours repeated under .dark:
--dt-summary-bg(--dt-surface-base),--dt-summary-border(--dt-border-subtle),--dt-summary-radius(--dt-radius-container),--dt-summary-padding(--dt-space-inset-lg): the panel.--dt-summary-divider(--dt-border-subtle): the rule above the total.--dt-summary-label-color(--dt-text-secondary),--dt-summary-value-color(--dt-text-primary): a line's label and figure.--dt-summary-muted-color(--dt-text-tertiary): a muted line.--dt-summary-hint-color(--dt-text-tertiary): hints.--dt-summary-discount-color(--dt-text-success): a discount's figure. It deliberately does not reuse--dt-price-sale-color(danger text): in a summary a discount is money saved, and red next to a promo field reads as an error. The minus sign carries the meaning, so colour is never the only signal.--dt-summary-total-color(--dt-text-primary): the total's label and figure.
Figures reach Price by re-pointing --dt-price-color on each figure, so a line's colour changes without changing how it is formatted. Figures are right-aligned with tabular numerals.
Accessibility
- The panel is a
<section>named by its heading. The breakdown is a<dl>: each label is a<dt>and its figure a<dd>, so a screen reader pairs them, and the total is the last pair. - A discount's figure is formatted as a negative ("-$13.80"), so it is read as money off without relying on colour.
- The free-shipping bar is a
role="progressbar"named by its message, witharia-valuenowandaria-valuemaxin money. - Figures come from
Intl.NumberFormatfor thecurrencyandlocale; passlocalewhen server rendering so the server and the browser print the same.
Content
- Line labels are sentence-case nouns: "Subtotal", "Shipping", "Estimated tax". Put a discount's code in its label: "Discount (SUMMER10)".
hintis a short fragment with no full stop: "Standard, 3โ5 days", "Calculated at next step".- Keep the total's label "Total" unless the page needs more: "Total due today", "Total (incl. VAT)".
Props
The typed contract, from OrderSummary.d.ts.
import * as React from "react";
/** One row of the money breakdown. */
export interface OrderSummaryLine {
/** What the amount is: "Subtotal", "Shipping", "Discount (SUMMER10)". */
label: string;
/** Amount in major units. A discount shows as negative whatever its sign; 0 shows as "Free". */
amount: number;
/** discount shows the amount as a negative in the discount colour; muted is for estimates and pending figures. @default "default" */
kind?: "default" | "discount" | "muted";
/** A smaller line under the label: "Standard, 3โ5 days", "Calculated at next step". */
hint?: string;
}
/** The money breakdown of a cart or checkout: lines, a total, an optional free-shipping bar and a footer for actions. */
export interface OrderSummaryProps extends Omit<React.HTMLAttributes<HTMLElement>, "children" | "title"> {
/** The rows above the total, in order. Pass what your cart computed; nothing is added up here. */
lines: OrderSummaryLine[];
/** The total, emphasised under a rule. */
total: {
/** @default "Total" */
label?: string;
/** Amount in major units. */
amount: number;
};
/** ISO 4217 currency code. @default "USD" */
currency?: string;
/** BCP 47 locale. Defaults to the runtime's; set it when server rendering. */
locale?: string;
/** Under the total: the checkout Button, a PromoCode, trust copy. */
footer?: React.ReactNode;
/** Heading of the panel, also its accessible name. @default "Order summary" */
title?: string;
/** Level of the title's heading element. @default 2 */
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
/** Shows a progress bar and "You're $12.00 away from free shipping", or "You've got free shipping" once current reaches threshold. */
freeShippingProgress?: {
/** The amount that counts toward the threshold, usually the subtotal. */
current: number;
/** The amount that unlocks free shipping. */
threshold: number;
};
}
export declare function OrderSummary(props: OrderSummaryProps): React.JSX.Element;
Tokens it reads
Every custom property OrderSummary.jsx resolves, read from the source: 12 component, 156 semantic. Component tier first, because that is the one to reach for.
- component Its own tier. Override one of these and only this component moves.
- semantic Shared roles. Override one and everything using that role moves with it.
| Token | Tier | Declared as |
|---|---|---|
--dt-price-color | component | var(--dt-text-primary) |
--dt-summary-bg | component | var(--dt-surface-base) |
--dt-summary-border | component | var(--dt-border-subtle) |
--dt-summary-discount-color | component | var(--dt-text-success) |
--dt-summary-divider | component | var(--dt-border-subtle) |
--dt-summary-hint-color | component | var(--dt-text-tertiary) |
--dt-summary-label-color | component | var(--dt-text-secondary) |
--dt-summary-muted-color | component | var(--dt-text-tertiary) |
--dt-summary-padding | component | var(--dt-space-inset-lg) |
--dt-summary-radius | component | var(--dt-radius-container) |
--dt-summary-total-color | component | var(--dt-text-primary) |
--dt-summary-value-color | component | var(--dt-text-primary) |
--dt-border-width-default | semantic | var(--dt-dim-hair) |
--dt-space-inline-md | semantic | var(--dt-dim-4) |
--dt-space-stack-2xs | semantic | var(--dt-dim-1) |
--dt-space-stack-md | semantic | var(--dt-dim-4) |
--dt-space-stack-sm | semantic | var(--dt-dim-3) |
--dt-space-stack-xs | semantic | var(--dt-dim-2) |
--dt-text-artboard-body-line | semantic | calc(var(--dt-line-height-md) * 2.35) |
--dt-text-artboard-body-size | semantic | calc(var(--dt-font-size-md) * 2.5) |
--dt-text-artboard-display-family | semantic | var(--dt-text-display-lg-family) |
--dt-text-artboard-display-line | semantic | calc(var(--dt-line-height-7xl) * 2.55) |
--dt-text-artboard-display-size | semantic | calc(var(--dt-font-size-7xl) * 2.8) |
--dt-text-artboard-display-tracking | semantic | var(--dt-tracking-tightest) |
--dt-text-artboard-display-weight | semantic | var(--dt-text-display-lg-weight) |
--dt-text-artboard-meta-size | semantic | calc(var(--dt-font-size-sm) * 2.2) |
--dt-text-artboard-meta-tracking | semantic | var(--dt-tracking-wider) |
--dt-text-artboard-title-line | semantic | calc(var(--dt-line-height-7xl) * 1.7) |
--dt-text-artboard-title-size | semantic | calc(var(--dt-font-size-7xl) * 1.75) |
--dt-text-body-lg-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-lg-line | semantic | var(--dt-line-height-lg) |
--dt-text-body-lg-size | semantic | var(--dt-font-size-lg) |
--dt-text-body-lg-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-body-lg-weight | semantic | var(--dt-font-weight-regular) |
--dt-text-body-md-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-md-line | semantic | var(--dt-line-height-md) |
--dt-text-body-md-size | semantic | var(--dt-font-size-md) |
--dt-text-body-md-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-body-md-weight | semantic | var(--dt-font-weight-regular) |
--dt-text-body-sm-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-sm-line | semantic | var(--dt-line-height-sm) |
--dt-text-body-sm-size | semantic | var(--dt-font-size-sm) |
--dt-text-body-sm-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-body-sm-weight | semantic | var(--dt-font-weight-regular) |
--dt-text-body-xs-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-xs-line | semantic | var(--dt-line-height-xs) |
--dt-text-body-xs-size | semantic | var(--dt-font-size-xs) |
--dt-text-body-xs-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-body-xs-weight | semantic | var(--dt-font-weight-regular) |
--dt-text-brand | semantic | var(--dt-color-primary-700) |
--dt-text-brand-secondary | semantic | var(--dt-color-secondary-700) |
--dt-text-code-md-family | semantic | var(--dt-font-family-mono) |
--dt-text-code-md-line | semantic | var(--dt-line-height-sm) |
--dt-text-code-md-size | semantic | var(--dt-font-size-sm) |
--dt-text-code-md-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-code-md-weight | semantic | var(--dt-font-weight-regular) |
--dt-text-code-sm-family | semantic | var(--dt-font-family-mono) |
--dt-text-code-sm-line | semantic | var(--dt-line-height-xs) |
--dt-text-code-sm-size | semantic | var(--dt-font-size-xs) |
--dt-text-code-sm-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-code-sm-weight | semantic | var(--dt-font-weight-regular) |
--dt-text-danger | semantic | var(--dt-color-red-800) |
--dt-text-disabled | semantic | var(--dt-color-neutral-400) |
--dt-text-display-2xl-family | semantic | var(--dt-font-family-sans) |
--dt-text-display-2xl-line | semantic | var(--dt-line-height-fluid) |
--dt-text-display-2xl-size | semantic | var(--dt-font-size-fluid-2xl) |
--dt-text-display-2xl-tracking | semantic | var(--dt-tracking-tightest) |
--dt-text-display-2xl-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-display-lg-family | semantic | var(--dt-font-family-sans) |
--dt-text-display-lg-line | semantic | var(--dt-line-height-7xl) |
--dt-text-display-lg-size | semantic | var(--dt-font-size-7xl) |
--dt-text-display-lg-tracking | semantic | var(--dt-tracking-tightest) |
--dt-text-display-lg-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-display-md-family | semantic | var(--dt-font-family-sans) |
--dt-text-display-md-line | semantic | var(--dt-line-height-6xl) |
--dt-text-display-md-size | semantic | var(--dt-font-size-6xl) |
--dt-text-display-md-tracking | semantic | var(--dt-tracking-tightest) |
--dt-text-display-md-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-display-sm-family | semantic | var(--dt-font-family-sans) |
--dt-text-display-sm-line | semantic | var(--dt-line-height-5xl) |
--dt-text-display-sm-size | semantic | var(--dt-font-size-5xl) |
--dt-text-display-sm-tracking | semantic | var(--dt-tracking-tighter) |
--dt-text-display-sm-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-display-xl-family | semantic | var(--dt-font-family-sans) |
--dt-text-display-xl-line | semantic | var(--dt-line-height-fluid) |
--dt-text-display-xl-size | semantic | var(--dt-font-size-fluid-xl) |
--dt-text-display-xl-tracking | semantic | var(--dt-tracking-tightest) |
--dt-text-display-xl-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-eyebrow-family | semantic | var(--dt-font-family-secondary) |
--dt-text-eyebrow-line | semantic | var(--dt-line-height-2xs) |
--dt-text-eyebrow-size | semantic | var(--dt-font-size-2xs) |
--dt-text-eyebrow-tracking | semantic | var(--dt-tracking-wider) |
--dt-text-eyebrow-weight | semantic | var(--dt-font-weight-semibold) |
--dt-text-heading-lg-family | semantic | var(--dt-font-family-sans) |
--dt-text-heading-lg-line | semantic | var(--dt-line-height-3xl) |
--dt-text-heading-lg-size | semantic | var(--dt-font-size-3xl) |
--dt-text-heading-lg-tracking | semantic | var(--dt-tracking-tight) |
--dt-text-heading-lg-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-heading-md-family | semantic | var(--dt-font-family-sans) |
--dt-text-heading-md-line | semantic | var(--dt-line-height-2xl) |
--dt-text-heading-md-size | semantic | var(--dt-font-size-2xl) |
--dt-text-heading-md-tracking | semantic | var(--dt-tracking-tight) |
--dt-text-heading-md-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-heading-sm-family | semantic | var(--dt-font-family-sans) |
--dt-text-heading-sm-line | semantic | var(--dt-line-height-xl) |
--dt-text-heading-sm-size | semantic | var(--dt-font-size-xl) |
--dt-text-heading-sm-tracking | semantic | var(--dt-tracking-snug) |
--dt-text-heading-sm-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-heading-xl-family | semantic | var(--dt-font-family-sans) |
--dt-text-heading-xl-line | semantic | var(--dt-line-height-4xl) |
--dt-text-heading-xl-size | semantic | var(--dt-font-size-4xl) |
--dt-text-heading-xl-tracking | semantic | var(--dt-tracking-tighter) |
--dt-text-heading-xl-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-heading-xs-family | semantic | var(--dt-font-family-sans) |
--dt-text-heading-xs-line | semantic | var(--dt-line-height-lg) |
--dt-text-heading-xs-size | semantic | var(--dt-font-size-lg) |
--dt-text-heading-xs-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-heading-xs-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-headline | semantic | var(--dt-text-primary) |
--dt-text-info | semantic | var(--dt-color-cyan-900) |
--dt-text-inverse | semantic | var(--dt-color-neutral-050) |
--dt-text-label-lg-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-lg-line | semantic | var(--dt-line-height-md) |
--dt-text-label-lg-size | semantic | var(--dt-font-size-md) |
--dt-text-label-lg-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-label-lg-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-label-md-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-md-line | semantic | var(--dt-line-height-sm) |
--dt-text-label-md-size | semantic | var(--dt-font-size-sm) |
--dt-text-label-md-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-label-md-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-label-sm-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-sm-line | semantic | var(--dt-line-height-xs) |
--dt-text-label-sm-size | semantic | var(--dt-font-size-xs) |
--dt-text-label-sm-tracking | semantic | var(--dt-tracking-normal) |
--dt-text-label-sm-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-link | semantic | var(--dt-color-neutral-900) |
--dt-text-link-brand | semantic | var(--dt-color-primary-700) |
--dt-text-link-brand-hover | semantic | var(--dt-color-primary-800) |
--dt-text-link-hover | semantic | var(--dt-color-neutral-700) |
--dt-text-link-visited | semantic | var(--dt-color-neutral-700) |
--dt-text-on-action | semantic | var(--dt-color-white) |
--dt-text-on-action-brand | semantic | var(--dt-color-white) |
--dt-text-on-action-brand-secondary | semantic | var(--dt-color-white) |
--dt-text-on-action-danger | semantic | var(--dt-color-white) |
--dt-text-on-action-disabled | semantic | var(--dt-color-neutral-400) |
--dt-text-on-action-ghost | semantic | var(--dt-color-neutral-900) |
--dt-text-on-action-secondary | semantic | var(--dt-color-neutral-900) |
--dt-text-on-brand | semantic | var(--dt-color-white) |
--dt-text-on-brand-muted | semantic | var(--dt-color-primary-900) |
--dt-text-on-brand-secondary | semantic | var(--dt-color-white) |
--dt-text-on-brand-secondary-muted | semantic | var(--dt-color-secondary-900) |
--dt-text-on-danger | semantic | var(--dt-color-white) |
--dt-text-on-info | semantic | var(--dt-color-white) |
--dt-text-on-scrim | semantic | var(--dt-color-neutral-050) |
--dt-text-on-scrim-brand | semantic | var(--dt-color-primary-200) |
--dt-text-on-scrim-secondary | semantic | var(--dt-color-neutral-200) |
--dt-text-on-scrim-strong | semantic | var(--dt-color-white) |
--dt-text-on-selected | semantic | var(--dt-color-neutral-900) |
--dt-text-on-selected-brand | semantic | var(--dt-color-primary-900) |
--dt-text-on-success | semantic | var(--dt-color-white) |
--dt-text-on-warning | semantic | var(--dt-color-neutral-950) |
--dt-text-primary | semantic | var(--dt-color-neutral-900) |
--dt-text-secondary | semantic | var(--dt-color-neutral-600) |
--dt-text-success | semantic | var(--dt-color-green-800) |
--dt-text-tertiary | semantic | var(--dt-color-neutral-500) |
--dt-text-warning | semantic | var(--dt-color-amber-900) |
--dt-text-wordmark | semantic | var(--dt-text-primary) |
Source
React only, no dependencies, every value a token.
import React from "react";
import { Price } from "./Price.jsx";
import { Progress } from "../feedback/Progress.jsx";
import { Heading } from "../typography/Heading.jsx";
const role = (name) => ({
fontFamily: `var(--dt-text-${name}-family)`,
fontSize: `var(--dt-text-${name}-size)`,
lineHeight: `var(--dt-text-${name}-line)`,
fontWeight: `var(--dt-text-${name}-weight)`,
letterSpacing: `var(--dt-text-${name}-tracking)`,
});
/* Label and figure colours per kind. A figure's colour reaches Price by
re-pointing --dt-price-color on its wrapper, so Price keeps its own
formatting and only the colour changes. */
const KINDS = {
default: { label: "var(--dt-summary-label-color)", value: "var(--dt-summary-value-color)" },
discount: { label: "var(--dt-summary-label-color)", value: "var(--dt-summary-discount-color)" },
muted: { label: "var(--dt-summary-muted-color)", value: "var(--dt-summary-muted-color)" },
};
/* The amount in the free-shipping message, formatted the way Price formats. */
function useMoney(currency, locale) {
return React.useMemo(() => {
try {
const nf = new Intl.NumberFormat(locale, { style: "currency", currency });
return (n) => nf.format(n);
} catch (err) {
return (n) => `${n} ${currency}`;
}
}, [currency, locale]);
}
export function OrderSummary({
lines = [],
total,
currency = "USD",
locale,
footer,
title = "Order summary",
headingLevel = 2,
freeShippingProgress,
style,
...rest
}) {
const titleId = React.useId();
const money = useMoney(currency, locale);
const fs = freeShippingProgress;
const reached = fs ? fs.current >= fs.threshold : false;
const message = fs
? reached
? "You've got free shipping"
: `You're ${money(Math.max(0, fs.threshold - fs.current))} away from free shipping`
: null;
const row = { display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: "var(--dt-space-inline-md)" };
const dd = { margin: 0, flex: "none", textAlign: "end", fontVariantNumeric: "tabular-nums" };
return (
<section
aria-labelledby={titleId}
style={{
display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-md)",
padding: "var(--dt-summary-padding)", boxSizing: "border-box", minWidth: 0,
background: "var(--dt-summary-bg)", color: "var(--dt-text-primary)",
border: "var(--dt-border-width-default) solid var(--dt-summary-border)",
borderRadius: "var(--dt-summary-radius)",
...style,
}}
{...rest}
>
<Heading id={titleId} level={headingLevel} size="heading-xs" tone="primary">{title}</Heading>
{fs && (
<Progress
size="sm"
tone={reached ? "success" : "primary"}
value={Math.min(Math.max(fs.current, 0), fs.threshold)}
max={fs.threshold}
label={message}
/>
)}
<dl style={{ margin: 0, display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-xs)" }}>
{lines.map((line, i) => {
const kind = KINDS[line.kind] || KINDS.default;
const discount = line.kind === "discount";
return (
<div key={line.label + i} style={row}>
<dt style={{ minWidth: 0, display: "flex", flexDirection: "column" }}>
<span style={{ ...role("body-sm"), color: kind.label }}>{line.label}</span>
{line.hint && <span style={{ ...role("body-xs"), color: "var(--dt-summary-hint-color)" }}>{line.hint}</span>}
</dt>
<dd style={dd}>
<Price
amount={discount ? -Math.abs(line.amount) : line.amount}
size="sm"
currency={currency}
locale={locale}
style={{ "--dt-price-color": kind.value }}
/>
</dd>
</div>
);
})}
<div
style={{
...row,
paddingBlockStart: "var(--dt-space-stack-sm)",
marginBlockStart: lines.length ? "var(--dt-space-stack-2xs)" : 0,
borderBlockStart: lines.length ? "var(--dt-border-width-default) solid var(--dt-summary-divider)" : undefined,
}}
>
<dt style={{ ...role("label-lg"), color: "var(--dt-summary-total-color)" }}>{total.label || "Total"}</dt>
<dd style={dd}>
<Price
amount={total.amount}
size="lg"
currency={currency}
locale={locale}
style={{ "--dt-price-color": "var(--dt-summary-total-color)" }}
/>
</dd>
</div>
</dl>
{footer && <div style={{ display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-sm)" }}>{footer}</div>}
</section>
);
}