Lists & feeds

Three distinct list patterns serve different data densities. The khata ledger row is dense and monospaced — built for financial data. The activity feed is conversational and prose-rich. The expense card list groups by date and leads with context. Each has its own empty state.

Transaction ledger (khata rows)

09:41
₹4,200
Farzi Cafe dinner
Pending
09:15
+₹840
Rohan settled
Settled
Yesterday
₹320
Swiggy order · 4 people
Settled
Mon
₹1,800
Goa Airbnb deposit
Pending
Mon
+₹600
Kabir settled Airbnb
Settled

The ledger row uses a four-column grid: 48px (time) · 72px (amount) · 1fr (label) · auto (tag). All font-family is JetBrains Mono — the one exception is the label column, which uses Onest body to keep long names legible.

Amounts are right-aligned in their column for vertical scanning. Positive amounts (money received / settled) are teal with a + prefix. Pending amounts are ink-primary — no coral, no red. Coral is reserved for the owe card in the settle flow.

Row dividers are dashed — border-bottom: 1px dashed var(--ink-line). This is the khata ledger language, not a generic separator. It differentiates the ledger from any other list in the product.

Smooth rolling queue

Live-updating ledgers (real-time activity during a group spend) use a single translateY animation on the track container rather than individual row animations. The whole stack slides up one row-height, then snaps back while swapping rows.

The top and bottom edges are masked with 36px paper→transparent gradient overlays. Rows fade in and out at the mask boundary, not at random per-row timings.

// Rolling queue logic
function addRow(track, rowEl) {
  track.appendChild(rowEl);
  const rowH = track.firstElementChild.offsetHeight;
  track.style.transform = `translateY(-${rowH}px)`;
  track.addEventListener('transitionend', () => {
    track.classList.add('snap');
    track.firstElementChild.remove();
    track.style.transform = 'translateY(0)';
    requestAnimationFrame(() => {
      requestAnimationFrame(() => track.classList.remove('snap'));
    });
  }, { once: true });
}

Activity feed

A
Aarav added "Farzi Cafe dinner" to Goa trip
2m ago
−₹1,400
P
Priya settled up with Aarav
14m ago
+₹840
Y
You added "Swiggy Biryani" to Flat expenses
1h ago
−₹320
K
Kabir joined Goa trip
Yesterday

Activity feed rows are conversational: avatar left, prose description center, amount right. The prose uses bold for person names, plain text for actions. Amounts use coral for what you owe (−₹) and teal for what you receive (+₹). Non-financial events (someone joined, a group was renamed) show a muted dash in the amount column.

Timestamp: JetBrains Mono 11px, muted. Relative time ("2m ago", "Yesterday") is preferable to absolute timestamps on the list — switch to absolute on tap to view the detail.

Expense list (grouped by date)

Today
🍜
Farzi Cafe dinner
Aarav paid ·
Y
A
P
3 people
₹4,200
you owe ₹1,400
🛵
Swiggy Biryani
You paid ·
Y
K
2 people
₹640
you get ₹320
Monday
Goa Airbnb
Priya paid ·
Y
P
A
K
4 people
₹18,000
settled ✓

Expense cards group by date, with date headers in JetBrains Mono uppercase. Each card shows: category emoji (in a warm-surface rounded square), expense name (Onest 600), payer + avatar group + count (muted), total amount (Mono 700), and your share with status.

"You owe ₹X" is coral. "You get ₹X" is teal. "Settled ✓" is teal muted — it's resolved, not requiring action. Never show "you owe" and "settled" in the same color family.

The avatar group in the meta line uses 20px avatars with 6px negative margin overlap and a paper border. Show max 4 avatars; if more, show "+2" in the same avatar style.

Empty states

No expenses yet
Split the first one — even the ₹40 chai counts.
All settled up.
Nothing to settle. Enjoy it while it lasts.
No activity
When expenses are added or settled, they'll show up here.

Empty states use a large muted glyph (35% opacity), a short title, one line of brand-voice copy, and an optional action button. The title and copy should be specific to the context — not generic "Nothing here yet."

The "all settled up" empty state has no action button. That's the point — there's nothing to do. Don't fill the silence with a prompt.

Usage

Do Use the ledger row for dense financial data where vertical scanning matters — transaction history, running totals, balance sheets.
Don't Don't use the ledger row for conversational content (activity, comments). The mono font and dashed dividers feel out of place for prose.
Do Group expense lists by date with mono uppercase headers. The grouping is part of the reading rhythm — it helps users find last weekend's dinner, not just scroll.
Don't Don't use infinite scroll without a stable anchor. When a new expense is added, the user should land at the top of today's group, not jump to a random position.
Do Show the "settled ✓" state on expense cards even after settlement — history builds trust. Users need to see that the balance resolved, not just that it disappeared.
Don't Don't hide settled expenses by default. Filtering is opt-in; the default view is the complete ledger.