---
title: "Icons & microcopy"
description: "Which icons Lopifi uses and how they size, color and change, plus the words a finance UI uses for buttons, errors, empty states and confirmations."
canonical: "https://lopifi.com/docs/foundations/icons-and-microcopy"
---

# Icons & microcopy

> Which icons Lopifi uses and how they size, color and change, plus the words a finance UI uses for buttons, errors, empty states and confirmations.

Source: https://lopifi.com/docs/foundations/icons-and-microcopy
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# Icons & microcopy

Which icons Lopifi uses and how they size, color and change, plus the words a finance UI uses for buttons, errors, empty states and confirmations.

---

## Icons

### The set
**Lucide**, 1.5px stroke, at 16 / 20 / 24. It's shadcn's default, MIT-licensed and comprehensive, and its optical grid matches Inter's rhythm.

```css
--icon-sm: 16px;   /* inline with body/label text, table cells, buttons */
--icon-md: 20px;   /* standalone buttons, nav items, card headers */
--icon-lg: 24px;   /* empty states, feature rows, mobile nav */
--icon-stroke: 1.5;
```

**Never scale an icon off-grid.** A 16px icon rendered at 18px has a 1.33px stroke and looks blurry against 1.5px siblings. Three sizes is enough.

<!-- plate:icons-sizes -->

### Stroke consistency
Stroke width is fixed at **1.5 across all sizes**. Lucide's default of 2 is too heavy next to Inter 400 at 14px and makes the icon shout over its own label. Set it once, globally:

```tsx
<LucideIcon strokeWidth={1.5} />
```

Scaling stroke with size (2px at 24px) is more correct optically, but it looks inconsistent when a 16px and a 24px icon share a view, which happens constantly. Fixed 1.5 keeps the set coherent.

### Alignment
Icons align to **cap height**, not line box. Offsets are in [grid & layout](https://lopifi.com/md/docs/foundations/grid-and-layout#optical-alignment). Lopifi ships an `<InlineIcon>` that applies the right offset for its size pair, because nobody remembers `-translate-y-[0.5px]` every time.

### Color
- Icons default to **`--text-secondary`**, one step down from their label. An icon at the same color as its text competes with it.
- An icon that *is* the only content (icon-only button) gets `--text-primary`.
- Semantic icons take the semantic color and **must not be the only signal**. A red circle without text is not a status.
- **Never `--accent` on an icon** unless it's inside the one primary action.

### Finance icon vocabulary
Consistency beats cleverness. These mappings are fixed:

| Concept | Icon | Note |
|---|---|---|
| Increase / up | `arrow-up-right` | Diagonal, not vertical — vertical reads as "sort" |
| Decrease / down | `arrow-down-right` | |
| Unchanged | `minus` | Not a dash glyph — an icon, for alignment |
| Transfer / move money | `arrow-right-left` | |
| Swap / convert | `arrow-left-right` in a circle | Rotates 180° on flip — see [motion catalog](https://lopifi.com/md/docs/motion/catalog) |
| Pending | `clock` | Never a spinner — pending is a state, not a load |
| Reconciled / matched | `check` in a circle | |
| Failed | `x` in a circle | |
| Needs review | `alert-triangle` | Amber. Reserve for genuine action-required. |
| Locked / final period | `lock` | |
| Recurring | `repeat` | |
| Attachment / receipt | `paperclip` | |
| Export | `arrow-down-to-line` | Not `download` (a cloud) — this is a file, not a fetch |
| Drill down | `chevron-right` | Rotates 90° when expanded |
| Bank account | `landmark` | |
| Card | `credit-card` | |
| Cash | `banknote` | |

**No emoji**, in any component. They render differently on every platform, can't be tinted, and make a financial product look unserious.

### Icon transitions
An icon that changes meaning **crossfades**, blur-masked, in 140ms, the same treatment as a changed value ([why the blur](https://lopifi.com/md/docs/motion/principles#blur-as-a-transition-mask)). It doesn't morph.

Path morphing (interpolating one outline into another, as in `raphaelsalaja/morphing-icons`) is good work, but wrong here. It needs both icons authored as one path with matching point counts, which Lucide doesn't provide and which breaks as soon as an icon is swapped. It animates `d`, which is neither `transform` nor `opacity`, on a surface that may have forty siblings. And it draws the eye to the transition, when what changed is the **state**, not the icon.

Two exceptions, both rotations instead of morphs. A rotation is a `transform`, free on the compositor:

| Case | Motion |
|---|---|
| Swap direction inverted | `arrow-left-right` rotates 180°, `spring.playful` |
| Disclosure opened | `chevron-right` rotates 90°, 140ms `--ease-standard` |

Everything else (pending → reconciled, unlocked → locked, synced → stale) is a crossfade. The vocabulary above is fixed, so a swap between two of its icons reads fine without animation.

### Icon-only buttons
Always an `aria-label`, always a tooltip, and always a hit area of at least 40px even when the icon is 16px (padding, or a pseudo-element hit-slop). This is the most common accessibility failure in dense toolbars.

```tsx
<button aria-label="Export transactions" className="size-10 grid place-items-center">
  <ArrowDownToLine className="size-4" strokeWidth={1.5} aria-hidden />
</button>
```

<!-- plate:icons-hit-area -->

Use `size-*` instead of `w-* h-*` for square elements. It's a baseline-ui rule, and it reads better.

### Brand and bank logos
Not icons. See [imagery](https://lopifi.com/md/docs/foundations/imagery-and-isometric#logo-treatment).

---

## Microcopy

Words are part of the design. In finance they're also part of correctness.

### Voice
**Precise, plain, and unhedged.** Not friendly, not clever, not apologetic.

| Don't | Do |
|---|---|
| "Oops! Something went wrong 😅" | "Couldn't load transactions." |
| "You're all caught up! 🎉" | "No transactions in this period." |
| "Awesome, payment sent!" | "Payment sent. Arrives 8 Sep." |
| "Are you sure?" | "Send $12,400.00 to Northwind Ltd?" |
| "Invalid amount" | "Enter an amount of $0.01 or more." |
| "Processing…" | "Submitted to bank · 2m ago" |

### The lexicon
One word per concept, used everywhere. Drift in wording is why financial products feel incoherent.

| Use | Never |
|---|---|
| **Transaction** (a movement of money) | activity, item, entry (reserve *entry* for journal entries) |
| **Balance** (a point-in-time amount) | total, funds, money |
| **Available** vs **Current** balance | "your money" — the distinction is material |
| **Pending** (submitted, not settled) | processing, in progress, on the way |
| **Settled** / **Cleared** | complete, done, finished |
| **Reconciled** (matched to a statement) | verified, confirmed, checked |
| **Categorized** | tagged, labeled, sorted |
| **Period** (a fiscal window) | date range, timeframe |
| **Statement** (P&L, BS, CF) | report — a *report* is something you generate |
| **Account** (a GL account or a bank account — always qualify which) | bare "account" in mixed context |
| **Payee** / **Recipient** (outbound) vs **Payer** (inbound) | customer, vendor, unless you mean specifically that |
| **Fee** (a charge we levy) vs **Cost** (spread, FX margin) | "charges" |
| **Rate** (FX) vs **Interest rate** | always qualify |

### Numbers in copy
- **Always the exact amount in a confirmation.** "Send $12,400.00", never "Send this payment".
- **Always the destination.** "to Northwind Ltd ••••4291".
- **Always the timing, if known.** "Arrives 8 Sep" beats "Sent successfully" because it answers the next question.
- **Never a number in a heading that also appears in the body.** Pick one.

### Errors
Per baseline-ui and [accessibility](https://lopifi.com/md/docs/craft/accessibility):
- **Next to where the action happened**, not in a toast at the corner of the screen. A field error goes under the field.
- **Say what to do**, not what failed. "Enter a valid routing number (9 digits)" not "Invalid input".
- **Never blame the user.** "That routing number isn't recognized" not "You entered an invalid routing number".
- **A toast is never the only channel for a critical error.** Toasts are dismissible, timed, and easy to miss. Persist the error inline.
- **Financial errors state the consequence.** "Payment not sent. No money has left your account." The second sentence matters most.

### Empty states
Always exactly one clear next action.

```
  [isometric illustration, ≤ 160px]
  No transactions yet                        title-sm  16/24 w500
  Connect a bank account to import           body      14/20 w400  text-secondary
  activity automatically.
  [ Connect account ]                        primary button
```

Three variants, each a different component:
- **Empty (first run)**: illustration plus one primary action. This is onboarding.
- **Empty (filtered)**: text only, no illustration, and a "Clear filters" action. An illustration here is condescending; the user knows what happened.
- **Empty (period)**: "No transactions between 1–30 Sep." Restate the resolved period and offer to widen it.

### Buttons
- **Verb + object.** "Send payment", not "Submit" or "Continue".
- **The destructive button names the destruction.** "Delete 42 transactions", not "Delete".
- **Never "OK".** The button says what it does.
- **Cancel is always available and never destructive-styled.**
- **Sentence case**, never Title Case, never ALL CAPS.

### Confirmations for irreversible actions
Use an `AlertDialog` that restates the three facts that matter: **amount, destination, timing**.

> **Send $12,400.00 to Northwind Ltd?**
> To account ••••4291 · Arrives 8 Sep
> This can't be undone once submitted.
> `[Cancel]` `[Hold to send →]`

<!-- plate:icons-confirm -->

For irreversible movements of money (wires, ACH after cutoff), confirmation is **hold-to-confirm**, not a click. The spec is in the [motion catalog](https://lopifi.com/md/docs/motion/catalog#hold-to-confirm). It's the one place we make an interaction *slower* on purpose, because the user should spend those 1.5 seconds deciding.

### Tone in loading and pending
Never "Please wait". State what's happening and, if you know it, how long:

- `"Importing 1,284 transactions…"`, with a determinate progress bar if you have a count
- `"Submitted to bank · settles in 1–2 business days"`
- `"Syncing with QuickBooks · last synced 4m ago"`

The last one shows the pattern: **current state + last known good state.** The UI stays useful while it's stale.
