DEV Community

solidifydev
solidifydev

Posted on

How I built a simple burn rate calculator with shadcn components

Hey there! Pull up a chair. I wanted to create a tool to see my cashflow clearly, so I set out to build a burn‑rate calculator that feels like a modern web app instead of a dusty spreadsheet relic, powered by shadcn/ui, React, Zod, and Recharts.

Screenshot of my cashflow over time

Starting with shadcn/UI & Radix Primitives

When you begin a new UI project, you want components that are:

  1. Accessible by default
  2. Composable without fighting styles
  3. Ready for interaction patterns

Enter shadcn/ui, which wraps Radix Primitives in a beautiful Tailwind‑based skin. For this app, my core layout and interactive pieces all came from there:

  • Card: A flexible container for both the timeline chart and the events table.
  • Popover: To surface the “Add Event” form without redirecting you or cluttering the page.
  • Tabs: Letting users switch between daily, weekly, and monthly event forms in a compact space.
  • Select, Input, NumberInput, Checkbox: Building blocks for clean, consistent form controls and filters.

Because Radix handles the accessibility––focus management, aria attributes, keyboard navigation––I could concentrate on wiring up state and logic, confident that folks using screen readers or keyboard alone would get a solid experience.

Defining Recurring “Events” with Zod

I wanted strong guarantees around the shape of each event. Zod’s discriminated unions made it a breeze:

const DailyEvent = z.object({
  type: z.literal("daily"),
  item: z.string(),
  amount: z.number(),
});
const WeeklyEvent = z.object({
  type: z.literal("weekly"),
  item: z.string(),
  amount: z.number(),
  weekday: Weekday,
});
const MonthlyEvent = z.object({
  type: z.literal("monthly"),
  item: z.string(),
  amount: z.number(),
  monthDay: z.number().int().min(1).max(31),
});
const zEventSchema = z.discriminatedUnion("type", [
  DailyEvent,
  WeeklyEvent,
  MonthlyEvent,
]);
Enter fullscreen mode Exit fullscreen mode

With this schema powering react hook form via zodResolver I got instant validation No more edge cases like February 31st sneaking in Plus TypeScript infers a perfect Event type for state and generator logic

Building the Interactive Table with React Table

Instead of hand rolling sorting and selection I leaned on @tanstack react table

  • ColumnDefs define headers accessors and custom cell renderers like formatting dollars in green or red

  • Row selection hooks into Radix checkbox so you can pick which events feed into your forecast

  • Pagination and filtering keep the table snappy even with dozens of events

  • Each time you check or uncheck a row the app recalculates your future balance in real time

Crafting the Time Series Generator

At the heart is a simple JavaScript generator

function* generateTimeSeries(
  events: Event[],
  startDate: Date,
  initialCapital: number
): Generator<{ date: Date; balance: number }, void, unknown> {
  let balance = initialCapital
  let current = new Date(startDate)
  while (true) {
    current.setDate(current.getDate() + 1)

    // Apply monthly events
    // Apply weekly events
    // Apply daily events

    yield { date: new Date(current), balance }
  }
}
Enter fullscreen mode Exit fullscreen mode

You call this generator once with selected events and initial capital then take N days and feed the result array into the chart.

A Gradient Split Area Chart with Recharts

I wanted visual feedback on good days versus bad days. With recharts, I defined a custom SVG gradient

<defs>
  <linearGradient id="splitColor" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stopColor="green" stopOpacity="0.5" />
    <stop offset={`${zeroOffset}%`} stopColor="green" stopOpacity="0.5" />
    <stop offset={`${zeroOffset}%`} stopColor="red" stopOpacity="0.5" />
    <stop offset="100%" stopColor="red" stopOpacity="0.5" />
  </linearGradient>
</defs>
<Area
  dataKey="balance"
  type="step"
  stroke="url(#splitColor)"
  fill="url(#splitColor)"
  fillOpacity="0.3"
  strokeWidth="2"
  dot={false}
/>
Enter fullscreen mode Exit fullscreen mode

zeroOffset is computed from min max balances so the color transition lands exactly where cash hits zero a glance shows whether you are trending into the red or cruising in the green

Putting It All Together

  1. Initial state with useState for initialCapital events timeRange and table state such as sorting and selection.

  2. Add and edit events using a popover housing the form built with Radix tabs and shadcn form components.

  3. Table and selection powered by React Table wired to Checkbox controls.

  4. Time series pulled from the generator based on selected rows.

  5. Chart rendering by recharts handling axes tooltips and that sleek gradient fill.

All logic lives in React hooks. No backend, no page reloads, and the users' data never leaves the browser.

And that is the tale of how I combined modern UI primitives strict schemas and custom time series logic to craft a burn rate calculator that feels more like an application than a spreadsheet. Hope you enjoyed this peek under the hood. You can try it out here: https://solidifydev.com/tools/burn-rate-calculator

Top comments (0)