Skip to content

Icons & microcopy


Icons

The set

Lucide, 1.5px stroke, at 16 / 20 / 24. It's shadcn's default, it's MIT, it's comprehensive, and its optical grid matches Inter's rhythm.

--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.

Stroke consistency

The stroke width is fixed at 1.5 across all sizes. Lucide's default is 2, which is too heavy next to Inter 400 at 14px — it makes the icon shout over its own label. Set it once, globally:

<LucideIcon strokeWidth={1.5} />

The alternative (scaling stroke with size, so 24px icons get 2px) is more "correct" optically but produces visible inconsistency when a 16px and a 24px icon appear in the same view, which happens constantly. Fixed 1.5 wins on coherence.

Alignment

Icons align to cap height, not line box. Offsets are in grid & layout. Lopifi ships an <InlineIcon> that applies the correct offset for its size pair, because relying on every consumer to remember -translate-y-[0.5px] doesn't work.

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 matters more than cleverness. Fixed mappings:

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
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. Anywhere. In any component. They render differently on every platform, they're untintable, and they instantly cost a financial product its seriousness.

Icon transitions

An icon that changes meaning crossfades, blur-masked, 140ms — the same treatment as a changed value (why the blur). It does not morph.

Path morphing — interpolating one outline into another, per raphaelsalaja/morphing-icons — is well-made work and wrong here. It needs both icons authored as a single path with matching point counts, which Lucide doesn't give us and which breaks the moment an icon is swapped; it animates d, which is neither transform nor opacity, on a surface that may have forty siblings; and its entire appeal is drawing the eye to the transition, when the thing that changed is the state, not the icon.

Two exceptions, and both are rotations rather than morphs — 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 members is already legible without being animated into.

Icon-only buttons

Always aria-label. Always a tooltip. Always ≥ 40px hit area even when the icon is 16px (padding, or a pseudo-element hit-slop). Non-negotiable — this is the most common accessibility failure in dense toolbars.

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

Note size-* rather than w-* h-* for square elements — a baseline-ui rule and it does read better.

Brand and bank logos

Not icons. See imagery.


Microcopy

The words are part of the design. In finance they're also part of the 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 here 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:

  • 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 reassurance is the most important sentence.

Empty states

Exactly one clear next action. Always.

  [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, and they are different components:

  • Empty (first run) — illustration + one primary action. This is onboarding.
  • Empty (filtered) — no illustration, text only, 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, 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

An AlertDialog, and it 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 →]

For genuinely irreversible movements of money (wires, ACH after cutoff), the confirm is hold-to-confirm, not click. Specification in motion catalog — it's the one place we deliberately make an interaction slower, because the user should be deciding for those 1.5 seconds.

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 is the pattern: current state + last known good state. It means the UI is still useful while it's stale.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.