Blog

Designing dense finance UIs for hospitality FP&A

Patterns for React finance grids, chart of accounts trees, and hospitality FP&A dashboards—lessons from anonymized enterprise work.

Hospitality finance is not spreadsheet-simple. A single property might run food and beverage, rooms, events, and shared services under one legal entity, with allocations crossing departments nightly. FP&A teams need to see actuals vs budget vs forecast at the GL level, drill into stat accounts, and export without losing the hierarchy they fought finance IT to preserve. When I build these surfaces in React and Next.js, “dense” is a feature, not a bug—until density becomes noise.

This post summarizes patterns I reuse across anonymized enterprise engagements: grid architecture, chart of accounts (COA) navigation, variance storytelling, and the hospitality-specific wrinkles that change how filters and time ranges behave.

Grids that survive real COA depth

A finance grid is not a generic data table. Users expect Excel-like affordances: pinned subtotals, expandable group rows, keyboard navigation across thousands of cells, and copy that preserves tab separators. I standardize on a virtualized grid layer so only visible rows mount, with column definitions generated from metadata rather than hand-written JSX per report.

Row identity is the subtle killer. GL lines are stable by account code, but grouped views inject synthetic subtotal rows. I assign deterministic IDs: group:{path} for aggregates and leaf:{entity}:{account}:{period} for postings. Without that discipline, React reconciliation flickers when users expand a region and the scroll position jumps.

type GridRow =
  | { kind: "group"; id: string; label: string; depth: number; expanded: boolean }
  | { kind: "leaf"; id: string; account: string; periods: Record<string, number> };

function rowKey(row: GridRow) {
  return row.id;
}

Formatting is centralized: negatives in parentheses for US hospitality groups, red only when variance exceeds materiality thresholds, and null vs zero distinguished in tooltips. Analysts notice immediately when zeros become dashes incorrectly.

Chart of accounts as navigation, not just a column

The COA is a tree, but reports flatten it. I treat COA selection as a first-class navigator—often a searchable tree panel synced with grid filters. Selecting “4xxx Operating Expenses” should filter leaves without hiding the parent context users need for orientation.

Search must understand account codes and plain language aliases (“banquet COGS” → 5210). Hospitality COAs also mix statistical accounts (covers, room nights) with monetary accounts; the UI labels them differently and blocks meaningless math on stat rows in variance columns.

Saved views are how power users survive: “US ops – week 42 – F&B actual vs budget” must restore column order, collapsed groups, and active filters. I store view state in the URL hash or query string where possible so screenshots and Slack links reproduce the same grid.

Hospitality FP&A semantics

Hotel and restaurant operators think in operating weeks, fiscal periods, and sometimes a 4-4-5 calendar. Date pickers cannot assume calendar months only. I expose period objects in the URL query string so shared links reopen the same consolidation window.

Property filters are multi-select with “portfolio” presets. All-inclusive resorts might eliminate certain revenue lines that full-service properties show. Rather than hard-code property types, I drive visibility rules from a lightweight entitlement map delivered with the dataset metadata.

Currency and unit mixing is common in global portfolios. Grids show native currency per property with optional consolidation into group currency using locked FX rates for the selected period. Rounding rules must match the GL—display rounding cannot drift from export rounding or month-end trust collapses.

Variance and commentary layers

Dense numbers need sparse words. I add a commentary column that opens a side panel—never an inline modal on top of the grid—so users do not lose scroll position. Variances color by threshold, but tooltips explain drivers: mix, rate, volume, or timing. Where ML-generated narratives existed, we still let humans edit and lock commentary per period to prevent accidental overwrites during close week.

Performance and export

Aggregations belong on the server or in a worker when datasets exceed tens of thousands of cells. The grid requests pages of leaves for the expanded path, not the entire COA at once. Export paths mirror on-screen hierarchy: CSV for analysts, formatted Excel for executives who will forward without opening the app.

Role-based entitlements trim columns as well as rows: property GMs see their site; regional directors see rollups; corporate FP&A sees adjustments and eliminations. The same component library renders all three; metadata drives visibility so we do not fork grids per persona.

Collaboration during close

Close week is concurrent editing under stress. Optimistic locking on commentary, presence indicators on rows with open notes, and audit trails on who changed a forecast cell reduce Monday-morning arguments. Real-time co-editing every cell is rarely worth it; targeted collaboration on exceptions is.

Anonymized lessons that transfer

Even without naming clients, the through-line is consistent: respect the COA as the mental model, virtualize early, encode period logic in types—not CSS—and never animate cells that accountants are trying to reconcile at 11 p.m. on month-end. Dense finance UIs win when they feel like the best part of Excel glued to the guardrails of a real application.