# Cart Styling

> Cart design contract, presets, tokens, responsive overrides, scoped custom CSS, named CSS blocks and every stable cart part.

Canonical URL: https://77865786950d:3005/cart/styling

Use typed `design_patch` first. Brand CSS variables remain available to the
cart, but ordinary page selectors, including page `data-part` overrides, are
excluded from the cart root and its children. Profile design settings and
custom CSS own cart styling.

## Cart design contract

Every profile has a `CartDesignSpecV1` document:

```json
{
  "schema_version": 1,
  "preset_id": "clean-default",
  "tokens": {},
  "shell": {},
  "modules": {},
  "responsive": {}
}
```

The three built-in presets are:

| Preset | Intended result |
|---|---|
| `clean-default` | Balanced default with standard spacing and controls |
| `soft-beauty` | Wider editorial layout, softer surfaces, larger media, and brand typography |
| `compact-utility` | Narrow, dense cart with compact line items and controls |

The compiler resolves the selected preset first, then applies explicit
overrides. The saved draft keeps the authored specification while preview and
production use the deterministic compiled result.

`design_patch` is a JSON Merge Patch: omitted fields remain, `null` removes a
stored override so the selected preset or parent value applies again, and
arrays replace arrays.

### Design tokens

`tokens` controls:

- Colors: background, surfaces, text, accent, border, success, warning, error,
  reward, and coupon
- Typography: brand/body/heading family, title and body scale, and weight
- Shape: radius, control radius, border width, and shadow
- Spacing: density and section gap

### Shell and module design

`shell` controls width, header layout, title, item-count treatment,
close-button style, sticky header/footer, and content background. Configure
frame appearance with `shell.panel`, `shell.overlay`, `shell.sheet`,
`shell.motion` and `shell.behaviour`.

`modules` can separately style reward progress, free shipping, cart lines,
coupon summary, product offers, order summary, checkout, payment options, and
the empty state. Cart-line design also controls media shape and size, dividers,
quantity and removal controls, sale-price treatment, and free-gift appearance.

### Responsive overrides

Use `responsive.mobile`, `responsive.tablet`, and `responsive.desktop` to
override tokens, shell settings, or module design at a specific viewport.
Responsive values merge over the selected preset and base profile design.
Select frame modes independently through `responsive.mobile.cart_mode`,
`responsive.tablet.cart_mode` and `responsive.desktop.cart_mode`. Top-level
`cart_mode:"headless"` omits DrawerShell and gives custom modules cart
bindings, lifecycle and managed commands.

### Example: preset with responsive overrides

Send this as `patch` in `lexsis_drafts.cart_edit`:

```json
{
  "design_patch": {
    "schema_version": 1,
    "preset_id": "soft-beauty",
    "tokens": {
      "colors": {
        "accent": "#8F4A5B",
        "surface_alt": "#FBF7F5"
      },
      "spacing": {
        "density": "spacious"
      }
    },
    "modules": {
      "cart_lines": {
        "variant": "editorial",
        "image_aspect": "portrait",
        "free_gift": {
          "variant": "gift_card",
          "badge_text": "A gift for you"
        }
      }
    },
    "responsive": {
      "mobile": {
        "tokens": {
          "spacing": {
            "density": "comfortable"
          }
        },
        "shell": {
          "width": "standard"
        }
      }
    }
  }
}
```

### Payment logos

Configure payment logos through `design_patch.modules.payment_options`:

```json
{
  "placement": "inside_checkout",
  "providers": ["upi", "visa", "mastercard"],
  "variant": "icons_with_labels",
  "alignment": "left"
}
```

Placement accepts `inside_checkout`, `below_checkout`, or `hidden`. Provider
order is preserved. Disabling the payment module hides its logos at either
placement. These settings change their display; they do not activate payment
methods at checkout. `commerce_config.payment_settings` is also editable with
`cart_promotions_edit`.

## Custom CSS

Custom CSS is:

- Stored with the profile draft
- Sanitized before saving
- Scoped under the resolved cart profile root
- Shared by preview and production rendering
- Published only when the merchant publishes the draft

Profile custom CSS is scoped under `[data-lx-cart-profile]` in the
`lx-cart-custom` layer, above `lx-cart-design` and island defaults.
Custom-module CSS has its own `@scope ([data-cart-custom-module="<id>"])`.
External CSS imports, external `url()` values, script escapes, and unbalanced
rules are rejected.

`custom_css` **replaces all custom CSS, including named blocks**. Passing
`null` clears it:

```json
{
  "patch": {
    "custom_css": "[data-part=\"panel\"] { border-radius: 0; }\n[data-part=\"checkout\"] { font-weight: 600; }"
  }
}
```

### Change one CSS block

For an incremental change, use `patch.css_ops`:

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 11,
  "patch": {
    "css_ops": [
      {"op": "upsert", "id": "checkout", "css": "[data-part=\"checkout\"] { font-weight: 700; }"},
      {"op": "remove", "id": "old-spacing"}
    ]
  }
}
```

Use an existing block ID for removal. Upserting keeps its original position;
new blocks append. The first incremental edit preserves existing replacement
CSS as a block named `legacy`. Blocks are concatenated in insertion order,
scoped to the cart, and validated individually and together. Do not send
`custom_css` and `css_ops` in the same patch.

## Module selectors

Use `[data-module="<layout-id>"]` for a particular module and
`[data-module-type="<layout type>"]` for a kind. Types use the layout's
snake_case values, for example `product_offer`. IDs match `layout_schema`,
including `offer-<uuid>`. Older generated sections used
`data-module="product-offer"`. The renderer preserves that legacy CSS selector
and also matches `data-module-type="product_offer"`, without editing or
republishing the stored profile. Use the type selector for new CSS.
Do not rename IDs in composition operations.
An after-line offer repeats the slot ID once per qualifying line.

Layout enabled flags are authoritative. Disabled modules are absent;
conditional modules use `hidden` while their predicate is false.
Offer visibility is owned by `layout_schema.modules[].enabled` and
`visible_when`. The `offer_slots` fields are compatibility projections of those
values. An explicit enabled edit in `cart_promotions_edit` updates the matching
layout module; unrelated edits preserve its visibility. An older generic
disabled offer stays disabled when normalized to slot-specific IDs.

Use this as a named `css_ops` block to address all offers and one custom module:

```css
[data-module-type="product_offer"] [data-part="heading"] {
  font-weight: 700;
}
[data-module="matching-item"] {
  margin-block: 12px;
}
```

## Drawer shell parts

Parts appear after the drawer first opens. These are not page sections.
Optional regions/controls exist only when enabled.

| `data-part` | Purpose | Inline styles |
| --- | --- | --- |
| `portal` | Profile root and portal mount | Pointer events and visibility |
| `overlay` | Backdrop close button | Animated opacity |
| `panel` | Drawer/dialog surface | Width/max-width, height/max-height, modal translate, animated transforms/opacity |
| `content` | Shell content column | None authored |
| `header-region` | Title row plus header modules | None |
| `header` | Header row | None |
| `header-title` | Visible `shell.title` | None |
| `header-count` | Quantity badge/inline count | None |
| `header-content` | Optional header module markup | None |
| `close` | Close button | Tap animation transform; circle is a class |
| `drag-handle` | Optional bottom-sheet handle | None |
| `committed-content` | Mounted cart content | None |
| `scroll-region` | Scrollable content and non-sticky regions | None |
| `body` | Body modules | None |
| `footer` | Footer module markup | None |
| `module-frame` | Direct built-in or custom module container, in composition order | None |
| `above-title` | Custom modules above the title, outside scrolling content | None |
| `floating-above` | Custom modules anchored above the panel, outside scrolling content | None |

The `panel` also carries `data-mode`, `data-header-layout`, `data-count-style`,
`data-close-style`, `data-sticky-header`, `data-sticky-footer`,
`data-content-background` and `data-desktop-layout`. `shell.title` supplies both
the visible header and dialog label. Do not replace the header with literal HTML.

## Cart island parts

Below are all named parts in the cart islands and their cart-specific child
controls. Scope common names like `root`, `item` and `bar` to the island's
module frame. Runtime-only parts appear in their active state.

| Island | Parts |
| --- | --- |
| CartLines | `root`, `empty`, `empty-visual`, `empty-message`, `empty-cta`, `line-group`, `item`, `item-image-wrap`, `item-image`, `reward-badge`, `item-info`, `item-title`, `adding-indicator`, `remove-btn`, `free-gift-label`, `item-variant`, `item-price`, `discount-percent`, `purchase-option`, `optimistic-qty`, `qty-controls`, `qty-select`, `qty-decrease`, `qty-value`, `qty-increase`, `reward-qty`, `module-frame` |
| CartSummary | `root`, `line`, `line-label`, `line-value`, `subtotal`, `subtotal-label`, `subtotal-hint`, `subtotal-value` |
| CartRewardProgress | `root`, `message`, `track`, `bar`, `milestones`, `milestone`, `milestone-threshold`, `milestone-visual`, `milestone-label`, `milestone-status`, `configuration-badge`, `scoped-offer`, `offer-condition`, `gift-title`, `gift-select`, `gift-action` |
| CartCoupons | `root`, `summary`, `savings`, `coupon-view` |
| CartCheckoutButton | `root`, `cta`, `payment-logos`, `secondary-cta` |
| CartCrossSell | `root`, `heading`, `fallback`, `item`, `item-image`, `item-info`, `item-title`, `item-price`, `add-button` |
| CartProgressBar | `root`, `visual`, `track`, `bar`, `message` |
| PaymentOptions | `root`, `provider`, `provider-logo`, `provider-fallback`, `button`, `text`, `icons-row` |
| Shared cart status | `cart-sync-status`, `cart-action-dots` |

CartCoupons portals `coupon-view` into the shell panel, outside its module
frame. Target `[data-part="coupon-view"]` under the profile, not only below
the coupon module. Its internal form/cards have no stable named
parts yet; do not invent selectors such as `coupon-input`.

PaymentOptions parts depend on its variant. Logos may be inside checkout,
below checkout, or hidden. The optional payment module's enabled/visibility
state controls those logos even when they render inside checkout.

CartLines exposes `unit-price` and `savings-chip` when its metadata settings
enable them. The compact reward rail exposes `compact-track`, `milestone-dot`,
`milestone-icon` and `milestone-captions`. Dots carry `data-state` and
`data-reward-id`; `--lx-reward-progress` supplies the bar percentage. Reward
toasts use `reward-toast` and a visible status message.

Compact milestone labels, icons and dots expose
`data-state="locked|active|unlocked"`. Set `modules.reward_progress.locked`,
`active` or `unlocked`: `text_color` colours the label and icon;
`border_color` and `background` colour the dot. The total hint has its own
`subtotal-hint` span, smaller and muted by default.

Cart modules use the same ancestry in the sandbox and published renderer.
Each island mounts through `[data-lx-island-runtime]`; it can share the module
frame or sit inside it, according to the profile's generated markup. A precise
child selector is
`[data-module="cart-lines"] [data-lx-island-runtime] > [data-part="root"]`.
Module descendant selectors also behave identically on both paths. Gift rows
follow paid rows in both renders.

### Inline-style exceptions

- CartLines, CartSummary and CartCheckoutButton: motion writes opacity,
  transforms and, during layout/exit animations, dimensions. Quantity/remove
  controls may have tap transforms. Style colors/spacing with the typed API.
- CartRewardProgress: `track` has measured top and optional segmented mask;
  `bar` adds live progress width; `milestones` has grid-template-columns;
  fallback `milestone-visual` has gradient/image backgrounds.
- CartProgressBar: `bar` uses `transform:scaleX(...)` for live progress.
- CartCoupons: `root` and `coupon-view` carry theme CSS variables;
  `coupon-view` also has motion transforms/opacity.
- PaymentOptions: `provider-fallback` sets the provider brand background color.
- CartVisual images set width, height and object-fit inline. Loading dots use
  inline animation delays. CartCrossSell itself adds no inline style.

Do not override live transforms/progress widths to change decoration. Inline
values beat normal styles, even in a later layer. Prefer typed values and
supported CSS variables; avoid broad `!important` rules. Dynamic visibility
rules deliberately enforce `[hidden]` so they cannot be defeated by layout CSS.

## Reward messages and celebrations

Reward messages are plain text. `messages.next`, `messages.all_unlocked` and
`messages.empty` accept these tokens:

| Token | Meaning |
|---|---|
| `{remaining}` | Formatted amount still needed for the next reward |
| `{next_title}` | Next reward's title, or empty when there is no next reward |
| `{threshold}` | Next reward's formatted threshold, or the final threshold |
| `{unlocked_title}` | Last applied reward's title, or empty before an applied reward |
| `{unlocked_list}` | Applied reward titles joined with “and” |
| `{reward_title}` | The reward being celebrated; valid in `toast.template` only |

Use a literal fallback with `{unlocked_title|Your rewards}`. Omit a whole clause
with a non-nested conditional:

```text
{#if unlocked}{unlocked_title} unlocked. {/if}{remaining} more gets you {next_title}.
```

Unknown tokens, unbalanced braces and nested conditional blocks fail validation.
Templates never evaluate JavaScript or produce HTML.

Configure animation and a visible toast with
`modules.reward_progress.celebration`; per-reward overrides live under
`modules.reward_progress.rewards[rewardId]`. `celebration.scope:"cart"`
confines particles to the panel. `"viewport"` places them above the overlay
and sheet, starting at the unlocked milestone dot or panel top. The canvas is
created synchronously before `cart:reward-unlocked` is dispatched; the browser
paints particles on animation frames.

Toasts stay within the panel's horizontal bounds. `toast.position` accepts
`top` (below the header), `bottom`, or `above_header`. When there is no room above
the panel, `above_header` falls back below the header. All positions avoid the
close button. `toast.duration_ms` controls toast lifetime independently; when
omitted it inherits `celebration.duration_ms`.

For visual QA, open the cart and call `window.__lx.celebrate(rewardId)`.
It replays the configured effect and emits the normal unlock event without
changing cart lines or discounts. It returns false for an unavailable reward
or disabled celebration. Reduced-motion preferences still apply.

Use dry run and signed fixture previews to inspect both populated and empty
carts; see [Profiles and Edit Loop](/cart/profiles#preview-a-sample-cart).

## Related

- [Cart Overview](/cart)
- [Profiles and Edit Loop](/cart/profiles)
- [Custom Cart Modules](/cart/custom-modules)
