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.
--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.
Three sizes, one stroke
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:
<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. 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
--accenton 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 |
| 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). 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.
<button aria-label="Export transactions" className="size-10 grid place-items-center">
<ArrowDownToLine className="size-4" strokeWidth={1.5} aria-hidden />
</button>
Small glyph, full target
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.
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:
- 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 →]
A confirmation says what will happen
For irreversible movements of money (wires, ACH after cutoff), confirmation is hold-to-confirm, not a click. The spec is in the motion catalog. 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.