26 KiB
Pitfalls Research
Domain: Personal finance dashboard UI overhaul (React SPA) Researched: 2026-03-16 Confidence: HIGH (most findings verified against official docs, Recharts issues tracker, and Tailwind v4 official docs)
Critical Pitfalls
Pitfall 1: Recharts Re-renders Every Parent State Change
What goes wrong:
The existing DashboardPage.tsx computes pieData and progressGroups directly in the render body — no useMemo. Every unrelated state change (e.g., a tooltip hover, a loading flag flip, QuickAddPicker opening) re-runs these array transforms and causes Recharts to diff its entire virtual DOM tree. Adding two more charts (bar chart, horizontal bar) multiplies this cost by 3x. With a large budget, each O(n) filter+reduce runs six times per render for each chart type.
Why it happens: The existing single-chart dashboard is fast enough that the missing memoization is invisible. During the overhaul, three chart instances are added to the same render tree, and the dashboard becomes visually complex enough that parent re-renders happen more frequently (collapsible state toggles, hover events on multiple chart tooltips).
How to avoid:
- Wrap all chart data derivations in
useMemowith explicit deps arrays:const pieData = useMemo(() => EXPENSE_TYPES.map(type => { ... }).filter(d => d.value > 0), [items, t] ) - Wrap formatter callbacks passed to
<Tooltip formatter={...}>inuseCallback. New function references on every render force Recharts to re-render tooltip internals. - Extract each chart into its own memoized sub-component (
React.memo) so only the chart whose data changed re-renders. - Create a category index Map once (in a hook or useMemo) and look up by key rather than
.filter()per item across three charts.
Warning signs:
- React DevTools Profiler shows
DashboardContentre-rendering on tooltip mouseover - Multiple chart tooltips fighting each other (Recharts issues #281 / #1770)
- "ResizeObserver loop limit exceeded" console errors when multiple
ResponsiveContainerinstances mount simultaneously
Phase to address: Dashboard redesign phase (charts implementation)
Pitfall 2: CSS Variable Scope — Recharts Cannot Read Tailwind @theme inline Variables
What goes wrong:
The existing palette.ts already uses var(--color-income) etc. as chart fill values. This works correctly because Tailwind v4's @theme inline inlines these as true CSS custom properties on :root. However, the risk is subtle: if any chart color is set via a Tailwind utility class string (e.g., fill-[var(...)]) rather than through the style prop or a direct CSS variable reference, Recharts SVG elements — which render outside the standard DOM paint context — may not resolve them correctly in all browsers.
Additionally, dark mode: the current CSS has no dark-mode overrides for --color-income through --color-investment. If the overhaul adds a dark mode toggle, all chart colors will remain light-mode pastel on dark backgrounds, creating very poor contrast.
Why it happens:
Tailwind v4's @theme inline block defines variables at :root scope, but the dark mode variant uses .dark class scoping. Theme variables defined only in @theme inline are not automatically duplicated under .dark {}. Dark-mode overrides for semantic colors (background, foreground) exist in shadcn's generated block, but category colors were custom-added without dark variants.
How to avoid:
- Pass all Recharts color values via JavaScript (the
fill={categoryColors[type]}pattern is correct — maintain it). - If dark mode is added in the overhaul: add a
.dark {}block inindex.cssthat overrides--color-income,--color-bill, etc. with darker/brighter variants appropriate for dark backgrounds. - Never attempt to pass Tailwind utility class strings as Recharts
fillprops. RechartsCell,Bar,Lineprops require resolved color values (hex, oklch string, orvar()reference). - Test chart colors in both light and dark modes before marking a phase complete.
Warning signs:
- Chart fills show as
oklch(...)literal text in DOM attributes instead of resolved colors - Colors are invisible or white-on-white on one theme variant
- Browser DevTools shows SVG
fillas unresolvedvar(--color-income)with no computed value
Phase to address: Design token / theming phase (early); dashboard charts phase (verification)
Pitfall 3: Collapsible Sections Causing Layout Shift and CLS Jank
What goes wrong:
The planned hybrid dashboard includes collapsible inline sections for each category group (income, bills, variable expenses, debt, savings). If these are implemented by toggling display: none / display: block or by animating the height CSS property from 0 to auto, the result is either: (a) instant snap with no animation, or (b) jank where the browser triggers full layout recalculations on every animation frame. With five collapsible sections and charts above them, collapsing a section causes the charts to resize (their ResponsiveContainer detects parent width change), triggering a cascade of resize events.
Why it happens:
Animating height from 0 to auto is a known browser limitation — you cannot CSS-transition to height: auto. Common naive workarounds (JavaScript measuring scrollHeight on every frame) cause layout thrashing. Radix UI's Collapsible component handles this correctly via CSS custom properties for height, but requires the data-state attribute pattern and the correct CSS transition on the inner CollapsibleContent element.
How to avoid:
- Use Radix UI
Collapsible(already available via shadcn) — it sets--radix-collapsible-content-heightas a CSS variable during open/close, enabling smooth CSS-only transition:[data-state='open'] > .collapsible-content { animation: slideDown 200ms ease-out; } [data-state='closed'] > .collapsible-content { animation: slideUp 200ms ease-out; } @keyframes slideDown { from { height: 0 } to { height: var(--radix-collapsible-content-height) } } - Add
debounce={50}to allResponsiveContainerinstances to prevent rapid resize recalculations when parent containers animate. - Use
paddinginstead ofmargininside collapsible children — margin collapsing causes jump artifacts on some browsers. - Never animate
height,padding, ormargindirectly with JavaScript setInterval; use CSS animations or Radix primitives.
Warning signs:
- Charts visually "snap" to a narrower width and back when a section above them is toggled
- Frame rate drops to under 30fps when expanding/collapsing sections (visible in DevTools Performance panel)
ResizeObserver loop limit exceedederrors spike after section toggle
Phase to address: Dashboard collapsible sections phase
Pitfall 4: Color Accessibility Failures in Financial Semantic Colors
What goes wrong: The most dangerous pattern in the existing dashboard is using pure green / red to signal positive / negative balance:
const balanceColor = availableBalance >= 0
? "text-green-600 dark:text-green-400"
: "text-red-600 dark:text-red-400"
And in the progress bar:
const barColor = group.overBudget
? "bg-red-500 dark:bg-red-400"
: "bg-green-500 dark:bg-green-400"
Pure green (#00FF00) has a contrast ratio of only 1.4:1 against white — catastrophically below WCAG AA's 4.5:1 minimum for text. Even green-600 (#16a34a) must be verified. Additionally, pie chart adjacent slice colors must maintain 3:1 contrast against each other (WCAG 2.1 SC 1.4.11 Non-text Contrast) — the existing six category colors are all similar lightness (OKLCH L ≈ 0.65–0.72), meaning they may be hard to distinguish for colorblind users or when printed.
Why it happens: Designers focus on making the palette "look rich" during an overhaul without running contrast checks. The redesign goal is "rich, colorful visual style" — this is a direct risk factor for accessibility regressions. Color-alone encoding (green = good, red = bad) violates WCAG 1.4.1 Use of Color, which requires that color not be the sole means of conveying information.
How to avoid:
- Run every text color against its background through a WCAG contrast checker (target 4.5:1 for normal text, 3:1 for large text and UI components). Use the Tailwind oklch values — compute with tools like https://webaim.org/resources/contrastchecker/
- For semantic colors (balance positive/negative, over-budget): supplement color with an icon or text label, not color alone. Example: a checkmark icon + green for positive balance; an exclamation icon + red for over-budget.
- For pie/donut chart slices: ensure adjacent colors have at least 3:1 contrast, or add a visible stroke separator (1px white/black stroke between slices provides a natural contrast boundary).
- For the 6 category colors: vary hue and lightness, not just hue. Consider making the OKLCH lightness values more spread (e.g., 0.55 to 0.80 range) so colors are distinguishable at reduced color sensitivity.
- Do not rely on
dark:text-green-400having passed contrast automatically — verify each dark mode color pair independently.
Warning signs:
- All six category pie slices are clearly distinguishable to the developer but indistinguishable when the browser's "Emulate vision deficiency" filter is applied in DevTools
- Running
window.getComputedStyleon a colored element and checking its OKLCH L value — if multiple semantic colors cluster at the same lightness, colorblind users see identical grays
Phase to address: Design token / visual language phase (establish accessible palette before building components); then verify in each component phase
Pitfall 5: i18n Key Regressions When Renaming or Adding UI Sections
What goes wrong:
The current translation files (en.json, de.json) have flat keys per page section. The overhaul adds new dashboard sections (bar chart, horizontal bar chart, collapsible income/bill/expense groups, richer donut legend). Each new label, section header, tooltip, and ARIA label needs a corresponding key in both files. During rapid UI iteration, developers commonly add t("dashboard.incomeSection") to the JSX, forget to add it to de.json, and only notice when the German locale shows the raw key string — or worse, the i18next fallback silently shows the English value and the German regression goes undetected.
Why it happens:
i18next with fallbackLng: 'en' (the existing config) silently falls back to English when a German key is missing. There is no visible failure. The project has no i18n linting step and no build-time key extraction check. The debug: false production config makes this invisible.
How to avoid:
- When adding a new UI section, add its keys to both
en.jsonandde.jsonin the same commit — never split across commits. - Use
i18next-cliori18next-scanner(npm package) to extract allt("...")call keys from source and diff against the JSON files. Run this as a pre-commit check or part of the build. - In development, consider setting
debug: trueini18n.ts— i18next logsmissingKeywarnings to console for every untranslated key in the non-fallback language. - Use a TypeScript-typed i18n setup (e.g.,
i18next-typescript) so thatt("dashboard.nonExistentKey")produces a type error at compile time. - Before marking any phase complete, manually switch locale to German and click through every changed screen.
Warning signs:
- German UI text contains raw dot-notation strings (e.g.,
dashboard.barChart.title) console.warnmessages containingi18next::translator: missingKey dede.jsonhas fewer top-level keys thanen.jsonafter a phase's changes
Phase to address: Every phase that adds new UI text; establish the key-parity check process in the first phase of the overhaul
Pitfall 6: Design Inconsistency Across Page Refreshes (The "Island Redesign" Problem)
What goes wrong: The overhaul covers all pages, but if phases are structured page-by-page, early pages get the richest design attention and later pages get inconsistency or fatigue-driven shortcuts. The most common failure mode: the dashboard uses a specific card style (colored header accent, icon in corner), but the Categories page — redesigned two weeks later — uses a subtly different card variant. Buttons on the Budget Detail page use different spacing than on the Template page. The result is a design that looks cohesive in screenshots of individual pages but feels broken when navigating between them.
Why it happens: There is no design system enforced at the component level. shadcn/ui components are used directly in pages without project-specific wrappers. When the overhaul introduces new visual patterns (e.g., a colored icon badge, a section divider style), they are implemented inline in the first page that needs them and then drift in subsequent pages as developers make minor "feels close enough" adaptations.
How to avoid:
- Before redesigning any pages, establish a small shared component library of new visual primitives (e.g.,
<SectionHeader>,<StatCard>,<CategoryBadge>) with fixed prop interfaces. All page redesigns consume these components — they never re-implement them inline. - Define the full color palette and spacing scale in the first phase, not incrementally. The
index.css@themeblock is the single source of truth — no hardcoded hex values anywhere else. - Create a visual spec (even a quick screenshot grid) of what each page will look like before coding begins, so inconsistencies are caught at design time not code review time.
- In code review: any new Tailwind classes that don't use design tokens (e.g.,
text-[#6b21a8],p-[13px]) should be flagged as violations.
Warning signs:
- Two pages use different visual patterns for "section with a list of items" (one uses a table, one uses cards)
- Color values appear as raw hex or oklch literals in component files instead of semantic tokens
- shadcn Card component is used in 3 different ways across 3 pages (no wrapper abstraction)
Phase to address: Design foundation phase (first phase of overhaul) before any page work begins
Technical Debt Patterns
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|---|---|---|---|
| Inline chart data transforms in render | Faster to write | Re-renders on every state change; adding more charts multiplies the cost | Never — always useMemo for chart data |
Hardcoded color classes text-green-600 for semantic states |
Familiar Tailwind pattern | Dark mode variants need to be manually maintained in two places; fails contrast checks | Only for truly non-semantic colors (e.g., a green checkmark icon that is always green) |
| Copying shadcn component patterns across pages without abstraction | Faster per-page development | Design drift guarantees inconsistency; changes to shared patterns require hunting all usages | MVP only, with explicit note to abstract before more pages are built |
| Adding English i18n keys without German equivalent | Unblocks development | Silent regression in German locale; accumulates debt that is hard to audit later | Never in this project — add both languages in the same commit |
Using ResponsiveContainer without debounce |
Default behavior, no extra code | Multiple containers trigger simultaneous resize cascades when sections open/close | Acceptable for single-chart pages; set debounce={50} on all multi-chart layouts |
Implementing collapsible with display: none toggle |
Simplest implementation | No animation; abrupt layout shift; screen readers cannot detect intermediate states | Never for primary dashboard sections; acceptable for debug/dev-only UI |
Integration Gotchas
| Integration | Common Mistake | Correct Approach |
|---|---|---|
| Recharts + Tailwind CSS variables | Passing Tailwind utility strings (e.g., "text-blue-500") as chart fill or stroke props |
Pass var(--color-income) or a resolved hex/oklch string via JS. Recharts SVG does not process Tailwind class strings. |
Recharts + ResponsiveContainer |
Placing ResponsiveContainer inside a flex/grid parent without an explicit height |
The container measures 0px height. Always wrap in a <div style={{ height: 240 }}> or give the parent an explicit height class. |
shadcn Collapsible + Recharts |
Placing charts inside collapsible sections without debounce |
When the collapsible opens, the chart container resizes and triggers ResizeObserver rapidly. Add debounce={50} to ResponsiveContainer. |
Tailwind v4 @theme inline + dark mode |
Defining category colors only in @theme inline without a .dark {} override block |
Category colors remain light-mode pastels on dark backgrounds. Define dark variants in a .dark {} block in index.css. |
| i18next + new UI sections | Adding t("new.key") JSX without adding the key to de.json |
Silently falls back to English in German locale. Always update both files simultaneously. |
Performance Traps
| Trap | Symptoms | Prevention | When It Breaks |
|---|---|---|---|
| Unmemoized chart data on a multi-chart dashboard | Tooltip hover causes all three charts to re-render simultaneously; visible lag | useMemo for all data transforms, useCallback for formatter props, React.memo on chart sub-components |
Noticeable with 50+ budget items; subtle with 10–20 |
| O(n) category lookup per budget item across 3 charts | CPU spike when dashboard loads; slow initial paint | Build a Map<id, Category> once in the hook layer, O(1) lookup in components |
Becomes visible at ~100 budget items |
Multiple ResponsiveContainer without debounce |
Rapid resize loop on section toggle; "ResizeObserver loop" errors | Add debounce={50} to all ResponsiveContainer instances on the dashboard |
Any time two or more containers are mounted simultaneously |
| Animating collapsible height with JavaScript | Dropped frames during expand/collapse; chart reflow cascade | Use Radix Collapsible with CSS @keyframes on --radix-collapsible-content-height |
Every interaction on the dashboard |
| TanStack Query unbounded cache growth | Long browser session consumes excessive memory | Set gcTime and staleTime on QueryClient config |
After ~30 minutes of active use without page reload |
UX Pitfalls
| Pitfall | User Impact | Better Approach |
|---|---|---|
| Color as the only signal for budget overrun (red progress bar) | Colorblind users cannot distinguish over-budget from on-track | Add an icon (exclamation mark) or text indicator alongside the color change |
| Hiding line items behind collapsible sections with no affordance | Users don't discover that sections are expandable; they think the dashboard is incomplete | Use an explicit chevron icon with visible state change; consider first-time-open hint |
| Chart tooltips showing raw numbers without currency formatting | Users see "1234.5" instead of "$1,234.50" — especially jarring in a financial app | Always pass formatCurrency(value, currency) in the Recharts formatter prop |
| Summary cards showing totals without context (no comparison to last month or budget) | Numbers without context are harder to interpret; users don't know if $1,200 expenses is good or bad | Show budget vs actual delta or a "vs budget" sub-label on cards |
| Five-section collapsible dashboard that defaults to all-collapsed | Users land on a nearly empty dashboard and are confused | Default the primary sections (income, bills) to open on first load |
| Progress bar clamped to 100% without showing actual overage amount | Users cannot see how much they are over budget from the bar alone | Show the actual percentage (e.g., "132%") in the label even when the bar is clamped |
"Looks Done But Isn't" Checklist
- Charts: Verify all three chart types (donut, bar, horizontal bar) render correctly when
itemsis empty — Recharts renders a blank SVG that can overlap other content if dimensions are not handled - Dark mode: Switch to dark theme (if implemented) and verify all category colors, chart fills, and semantic state colors (green/red) have sufficient contrast — light-mode testing only is the most common gap
- German locale: Navigate every redesigned page in German and verify no raw key strings appear —
de.jsonparity check - Collapsible sections: Toggle each collapsible section open and closed rapidly three times — verify no layout shift in charts above/below, no "ResizeObserver loop" errors in console
- Empty states: Load the dashboard with a budget that has zero actual amounts — verify charts handle
pieData.length === 0and empty bar data without rendering broken empty SVGs - Long category names: Enter a category named "Wiederkehrende monatliche Haushaltsausgaben" (long German string) — verify it doesn't overflow card boundaries or truncate without tooltip
- Currency formatting: Verify EUR formatting (€1.234,56) and USD formatting ($1,234.56) both display correctly in chart tooltips and summary cards when locale is switched in Settings
- Responsive at 1024px: View the dashboard at exactly 1024px viewport width — the breakpoint where desktop layout switches — verify no horizontal overflow or chart sizing issues
Recovery Strategies
| Pitfall | Recovery Cost | Recovery Steps |
|---|---|---|
| Chart re-render performance discovered in production | LOW | Add useMemo wrappers to chart data transforms; wrap chart components in React.memo; requires no visual changes |
| Color accessibility failures discovered post-launch | MEDIUM | Audit all semantic color uses with WCAG checker; update index.css CSS variable values; may require minor component changes for icon-supplement approach |
| i18n key regressions across multiple pages | MEDIUM | Run i18next-scanner to enumerate all missing German keys; systematically add translations; no code changes needed |
| Design inconsistency across pages after all pages are shipped | HIGH | Requires extracting shared component abstractions retroactively and refactoring all pages — avoid by establishing components in first phase |
height: 0 → auto collapsible causing jank discovered mid-phase |
LOW | Replace toggle logic with Radix Collapsible and add CSS keyframe animation — isolated to the collapsible component, no broader refactor needed |
ResponsiveContainer ResizeObserver loop in multi-chart layout |
LOW | Add debounce={50} prop to each ResponsiveContainer — one-line fix per chart instance |
Pitfall-to-Phase Mapping
| Pitfall | Prevention Phase | Verification |
|---|---|---|
| Recharts re-render on every parent state change | Dashboard charts phase — add memoization before wiring data | React DevTools Profiler: chart sub-components should not appear in flame graph on tooltip hover |
| CSS variable scope / dark mode chart color gaps | Design tokens phase (first) | Inspect SVG fill in DevTools; toggle dark mode and visually verify all chart colors resolve |
| Collapsible layout shift and ResizeObserver cascade | Dashboard collapsible sections phase | Toggle all sections rapidly; check console for ResizeObserver errors; check DevTools Performance for layout thrash |
| Color accessibility failures | Design tokens phase — define accessible palette upfront | Run each color pair through WCAG contrast checker; use DevTools "Emulate vision deficiency" filter |
| i18n key regressions | Every phase — enforce dual-language commit rule from the start | Run i18next-scanner before marking any phase complete; switch to German locale and navigate all changed pages |
| Design inconsistency across page refreshes | Design foundation phase — create shared components before any page work | Code review: flag any Tailwind color/spacing classes that are not semantic tokens; check that all pages use shared <SectionHeader> / <StatCard> etc. |
Sources
- Recharts performance guide (official)
- Recharts deep-compare issue #281
- Recharts ResizeObserver loop issue #1770
- Recharts ResponsiveContainer API
- Improving Recharts performance (Belchior)
- Tailwind v4 dark mode docs
- shadcn/ui Tailwind v4 guide
- Tailwind v4 migration breaking changes discussion
- shadcn theming with Tailwind v4 and CSS variables (Goins)
- WCAG 2.1 SC 1.4.11 Non-text Contrast
- WCAG SC 1.4.3 Contrast Minimum
- WebAIM contrast checker
- Radix UI Collapsible primitive
- i18next missing key detection discussion
- Fintech dashboard design (Merge Rocks)
- Dashboard UX design principles (Smashing Magazine)
- Existing codebase analysis:
src/pages/DashboardPage.tsx,src/lib/palette.ts,src/index.css,.planning/codebase/CONCERNS.md
Pitfalls research for: SimpleFinanceDash UI overhaul (React + Recharts + Tailwind v4 + shadcn/ui) Researched: 2026-03-16