Cart Line Item
ProA single cart row: image, title, variant, quantity stepper and a computed line total, with remove and save-for-later actions. Bursts of quantity clicks settle into one write, which rolls back if the server rejects it. Stock caps the stepper rather than only hinting, and a price that moved since the item went into the cart says so. Default and compact densities.
Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.
import { SvCartLineItemComponent } from '@styloviz-pro/commerce';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Read-only (order details)
ProLow-stock hint + save for later
ProPersisting one write per burst of clicks
ProRemoved, with the undo window the host owns
ProA variant that stopped being buyable
ProStock as a cap, and a cart that already holds too many
ProA price that moved since the item went into the cart
ProWhen to use
- Rows in the cart page and the mini-cart drawer.
- A read-only line in order details / confirmation (set `editable`/`removable` to false).
- You want the line total to track quantity automatically.
Async states
- error
Server writes
- Writes
- quantityCommit
Each payload carries a
requestKey— send it with the request so a retry cannot apply the same change twice.- Rollback
- Call
event.rollback()when the save fails — the change is already on screen, and this is what puts it back. - While saving
- Bind
saving— with two writes outstanding there is no single state to roll back to, so the action stays locked until the first one lands. - On failure
- Pass the failure to
errorto show it in place — without taking away what the user was working on.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | — | Product title. |
| unitPricerequired | number | — | Unit price (minor units when `minorUnits` is true). |
| image | string | '' | Product image URL. |
| href | string | '' | Link target for the product. |
| variantLabel | string | '' | Variant description, e.g. "Black / M". |
| compareAt | number | null | null | Compare-at unit price for a strikethrough. |
| currency | string | 'USD' | ISO 4217 currency code. |
| locale | string | 'en-US' | BCP-47 locale. |
| minorUnits | boolean | false | Treat prices as minor units. |
| min | number | 1 | Minimum quantity. |
| max | number | 99 | Maximum quantity. |
| stock | number | null | null | Remaining stock, as the server last reported it. `null` is "not known". THIS IS A CAP, NOT A HINT. Stock is the number most likely to change while the row is on screen — the cart was filled an hour ago and somebody else has been shopping since. It used to only produce a "Only 3 left" nudge below 6, which left the stepper happily climbing to 99 and a line total quoting a price for goods that do not exist. It now caps the stepper (`quantityMax`), and a quantity already above the cap gets said out loud rather than silently rewritten: the cart holds what it holds, and correcting it is a write this row does not make. |
| previousUnitPrice | number | null | null | What this item's unit price was when it went into the cart, if it has changed since. A PRICE THAT MOVES UNDER A SHOPPER HAS TO SAY SO. `unitPrice` is an input, so a host applying a fresh quote silently redraws the line total and the shopper is asked to pay a number they never agreed to — the same failure `sv-order-summary` handles with its `repriced` state, one level down. Set this to the old price and the row names the change and its direction. It is display-only: it does not affect the line total, which is always `unitPrice × quantity`. |
| editable | boolean | true | Show the quantity stepper (false renders a static "Qty: n"). |
| removable | boolean | true | Show the remove control. |
| showSaveForLater | boolean | false | Show a "Save for later" control. |
| size | CartLineSize | 'default' | Density. |
| showMedia | boolean | true | Show the product image (or its placeholder) at the start of the row. `false` is for lines where the shopper is not choosing by sight — a till ticket, a returns form, a re-order list. There the empty image box is 64px of row that carries nothing, on the one screen that needs every line it can fit. |
| saving | boolean | false | True while a write for this row is in flight. The stepper and both row actions stop responding, so a second edit cannot race the first and leave `rollback()` ambiguous. |
| error | SvDataError | string | null | null | The last write for this row failed. Rendered as a danger note under the title. Roll the quantity back as well — this input reports the failure, it does not undo it. |
| commitDelayMs | number | 400 | How long the quantity settles before it is reported, in milliseconds. `0` reports every step immediately, which is what the row used to do. |
| unavailable | boolean | false | This variant can no longer be bought — discontinued, or out of stock since the cart was filled. The row still renders, because a shopper needs to see what they are being asked to drop, but the quantity is frozen and the line total is struck: it is not part of the payable order. |
| unavailableText | string | 'No longer available' | Copy for the unavailable note. |
| removed | boolean | false | The host has removed this row and is offering a way back. The row renders as removed with an Undo control, and nothing else responds. The window is the host's to open and close — a component that started its own timer and committed the deletion when it expired would be claiming a write it never makes. |
| quantity | number (two-way) | 1 | Quantity (two-way: `[(quantity)]`). Moves on every click so the stepper and the line total stay live. What to PERSIST comes from `quantityCommit`, once the clicking stops. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| quantityCommit | CartQuantityCommitEvent | The quantity settled. One emission per burst of edits — persist this. |
| remove | void | Emitted when the remove control is pressed. |
| undoRemove | void | The shopper pressed Undo on a removed row. |
| saveForLater | void | Emitted when "Save for later" is pressed. |
| quantityChange | number | Emits the new `quantity` whenever it changes — the output half of `[(quantity)]`. |
Customization & theming
- Default and compact densities.
- Editable quantity stepper or a static quantity.
- Optional remove and save-for-later actions.
- `commitDelayMs` tunes how long a burst of clicks settles; `0` reports every step.
- `unavailableText` for why a variant can no longer be bought.
- `stock` caps the stepper; `previousUnitPrice` turns on the repricing note, with the direction and old figure formatted in the row's own currency and locale.
- Any ISO currency + locale, with minor-unit support.
Best practices
- Do: Bind `[(quantity)]` for display and persist from `(quantityCommit)`. The model moves on every click; the commit fires once the clicking stops.
- Do: Call `rollback()` from the commit payload when the write is rejected. It restores `previousQuantity` — the value the server last agreed to, not the value one click ago.
- Do: Send `requestKey` with the write and ignore a key you have already applied, so a retry cannot charge for a quantity twice.
- Do: Bind `[saving]` while a write for the row is in flight. It freezes the stepper and both actions, which is what keeps `rollback()` unambiguous.
- Do: Pass `stock`. It is a cap, not a hint: the stepper stops at `min(max, stock)`, a low-stock note appears near the limit, and `0` makes the row unbuyable without a separate `[unavailable]`.
- Do: Bind `previousUnitPrice` when a fresh quote changed the price. Otherwise the line total silently redraws and asks the shopper to pay a figure they never agreed to.
- Do: Own the undo window yourself: soft-remove on `(remove)`, set `[removed]="true"`, and restore on `(undoRemove)`. The row will not start a timer and commit a deletion on your behalf.
- Don't: Don't fire a request from `(quantityChange)` — three taps on `+` become three requests for 2, 3 and 4 against a server free to apply them in any order.
- Don't: Don't compute the line total yourself — it is derived from unit price × quantity.
- Don't: Don't expect the row to fix an over-stock quantity. It reports the conflict; clamping the cart is a write, and writes belong to you.
- Tip: Call `commitNow()` from a Checkout button so a still-settling edit is not lost on navigation.
Accessibility
- Remove, undo and save actions are real buttons with descriptive labels naming the product.
- The quantity stepper is named after its line, so a five-item cart does not announce "Number input" five times — and its + / – buttons are named after the line too, not "Increase value".
- Whichever note the row is showing describes the stepper via `aria-describedby`, with a per-instance id so one row is never described by another's. A quantity that outruns the stock, or a write that failed, also sets `aria-invalid` — so the red border and the announced state agree.
- A write in flight sets `aria-busy` on the row and disables its controls.
- A failed write is text in a `role="alert"`, never colour alone; removed and unavailable rows say so in words as well as by dimming the image. The repricing note names the direction in words, so up and down are not told apart by colour.
- Image links and titles are keyboard-reachable; line total is tabular-nums text.
Feedback on Cart Line Item
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.