Charge list

The three months of past charges on a subscription's own screen: a date on the left, an amount on the right, one row per charge. It is the evidence under every other number on that page, and it is the only list in the product that can mark a single row.

Molecule5 grey pages5 coloured1 state, and it is a state of the data

Anatomy

A molecule that hosts two atoms and repaints neither. Everything below is drawn by design/system/components/charge-list.css through the same entry point a product screen loads. On a real screen this list sits inside the Panel organism, at its own page: the panel supplies the white surface, the hairline and the clipped corners, and that is why the rows carry their own side padding while a pair list does not.

  • Aug 3, next$17.99was $15.49
  • Jul 3$15.49
  • Jun 3$15.49
  • May 3$15.49

Variants and sizes

One axis, and it is a state of the data rather than a variant of the component. There is one charge row and one size of it; what changes is whether the product is pointing at a particular row and why. The empty cells below are the tones this component is not allowed to take, each with the reason, because a list of money is exactly where a system reaches for red if nobody has written down that it may not.

Row

Marked when the product has something to say about that one charge, and only then. Two marked rows in three months means the screen is telling two stories and the alert belongs above the list instead

Tokens Where it stands
Rest .charges li --line-divider
--type-body
All five screens. Three identical months is the calm answer: nothing has changed, and the list says so by being boring
  • Jul 7$11.99
  • Jun 7$11.99
Marked, a price change li.marked --bg-attention
--text-muted (.was)
The price change state. The marked row is the NEXT charge, not a past one: the news is what is about to happen
  • Aug 3, next$17.99was $15.49
  • Jul 3$15.49
Marked, a payment that failed li.marked the same two The payment failed state. Same class, same wash, and the slot under the amount carries a phrase instead of a price
  • Jul 20$14.99did not go through
  • Jun 20$14.99
Loading .charges .skel --bg-placeholder
56px slot
The loading state. Three rows of two bars, and aria-hidden on the list: the state block above already says the page is loading
A clay row - Forbidden with a reason. Clay is OUR failure and nothing else, scoped by DESIGN.md to the sync error and the could-not-load screen. A payment that did not go through at the merchant is not our failure and not the person's: it is a fact about the world, and the copy on that screen says so in as many words. It keeps the amber
A red row - Forbidden, and it is the one prohibition in this system that came from a recorded quote rather than from taste: the persona closes any app that shows her red. There is no red in the palette to reach for

The old price is not struck through, and that was checked rather than assumed. There is no text-decoration on .was, no <s> and no <del> anywhere in this project. The previous price is announced by the word: "was $15.49". That is the right answer twice over. A line through text is not reliably announced by a screen reader, so a struck price reads aloud as a second, current price sitting next to the real one; and <del> would be a lie, because nothing was deleted, the price changed. The word carries the meaning to everybody in the same order, which is also why the same slot can say "did not go through" without needing a second rule.

The last row draws no rule, and that changed on 2026-08-12. A divider divides two things, and after the last row there is nothing to divide. The founder's review of the components made that a principle of the system rather than a patch on one seam, and this component was the only place in the system it had to be applied: every other ruled list already ended this way (.card.prose li:last-child, .pairs div:last-child, .pairs li:last-child, .cand:last-child, .switch:last-of-type, .rstep:last-of-type), and the two lists built on li + li never drew a last rule at all.

It closes two doubled seams, not one, and the second is the reason a principle beats a patch. Measured in a browser at 360 and 1280, before and after. On the four subscription-detail states that carry a Pro gate, the last row's border-bottom: 1px rgb(238, 242, 243) and the gate's own border-top of the same value touched at exactly 0.00px, so that seam carried 2px of --line-divider where every other divider in the panel carries 1. On the loading state there is no gate: the list is the panel's last child, and the same rule sat flush at 0.00px against the panel's own bottom edge, 1px of --line-divider stacked on 1px of --line-container. Nobody had named the second one. Closing the seam by taking the rule off the gate instead would have left it standing.

The list still reads as closed, which was checked and not assumed. In the product it is always inside the Panel, and a bordered container closes a list by itself: in the first case the gate's own top rule is still drawn, in the second the panel's own hairline is. No occurrence anywhere in the product relies on its last row to look finished. Measured effect: the panel is 1px shorter on all five subscription-detail screens at both viewports, and no other geometry moves.

When to use it

This list is the product showing its working. Everything else on a subscription's screen is a conclusion: this is $11.99 a month, the next charge is Aug 7, you have paid $143.88 since last August. The charge list is where those conclusions come from, and a person who does not believe the summary can check it here in three lines. That is the fourth design principle, trust through transparency, written as a component: show the source of every figure.

Three months, and three months exactly, because that is what the free tier shows. The Pro gate sits directly under this list, never in front of it, which is decision D3: the paywall is at depth, never at basic visibility. The list is what a person came for; the gate is an offer about seeing more of it.

The marked row is the emotional centre of the whole screen. On the price change state it is the next charge, marked amber, with the old price under the new one, and the sentence above the list has already said what happened in words. The colour is doing the same job the copy is doing: a price going up is news, not an emergency, and both have to say that before the number does.

The rule, and the anti-rule

One marked row, and the reason under the number
  • Aug 3, next$17.99was $15.49
  • Jul 3$15.49
  • Jun 3$15.49

The wash points at the row, and the row says why in five characters plus a price. Nothing else is added: no icon, no border, no bold. One mark for one message, which is the same rule the wash block follows and the reason neither of them needs a third signal.

Use a wash block instead
Netflix went up by $2.50 on Jul 28. Your next charge is $17.99 instead of $15.49.

The explanation of what happened is a wash block above the list, not a longer line inside it. A charge row has room for a date, an amount and about four words; a sentence squeezed into it would wrap, break the baseline the column depends on, and still say less than the banner does. The list points; the banner explains.

A charge is a fact, not a target
  • Jul 12$14.00
  • Jun 12$14.00
  • May 12$14.00

No row here is a link, on any of the five screens. A past charge has nowhere to go: there is no per-charge screen in this product and there is not meant to be. Making the rows tappable would promise one.

Use a subscription row instead
Spotify Premium · Aug 7 · $11.99 ›

When the line IS a way into something, it is a subscription row inside a divided list: 56px tall, the whole row one anchor with one accessible name, a merchant logo at the front. The two look similar in a screenshot and are opposite in what they promise. The test is whether there is a screen behind the line.

States

None, in the interaction sense, and it was checked rather than assumed. A sweep of every .charges list on all 57 grey pages and all 57 coloured ones returns zero anchors inside one, 5 lists on each side, and no row is hoverable, focusable or pressable. li.marked is a state of the DATA and is declared with the variants above, where it can stand next to the two events it carries: putting it in a row with hover and focus would suggest a person can cause it, and nobody can cause a price change by pointing at it.

The technical half

ReadsWhich tokenGrowing from
the marked row's wash--bg-attention--amber-wash, and --amber-wash-dark in the dark theme
the note under an amount--text-muted--muted. The single secondary tier, 5.8:1 on the surface light and 6.4:1 dark. On a marked row it sits on the amber wash instead, which is the one pair on this component the token audit measures separately
the divider--line-divider--hairline-soft. Decorative: nothing here has to be found by touch. Between rows only, never after the last one
the row, the note--type-body, --type-meta14px and 12px. The row was 13.5, folded by the founder's review of 2026-08-11
the row's padding and gap--space-8, --space-16the 8px grid. 10 and 14 both fold, and the side padding still lines up with the panel head, which was also written as 14
the skeleton slotliteral56px. A width is not a spacing, and skeleton-bar.css hands this one here by name along with the pair list's two
the amountnot read herethe Amount atom owns its own size, weight and ink. This file positions the row around it and repaints nothing

Lives in design/system/components/charge-list.css. Stands on 5 grey pages and the same 5 in colour: Subscription Detail, price change, payment failed, unrecognized, loading.

<ul class="charges"> <li class="marked"><span>Aug 3, next</span><span class="amt">$17.99<span class="was">was $15.49</span></span></li> <li><span>Jul 3</span><span class="amt">$15.49</span></li> <li><span>Jun 3</span><span class="amt">$15.49</span></li> </ul>