Installment Calculator (React)
An inline installment calculator React component for product pages: 3/6/12/24-month segmented switch, annual rate and fees, a rolling monthly amount, total and interest, effective annual rate, a 0% badge and an expandable per-period schedule.
The "as low as NT$XXX per month" line you see on every Taiwanese e-commerce product page. Pass in the product price and a list of plans (months, annual rate, fee) and the component computes the monthly payment with the standard amortization formula and shows it as a digit-by-digit rolling number. Switching the term slides the selected pill across, the total / interest + fees / effective annual rate update in step, and zero-interest plans automatically get a "0 利率" (0% interest) badge. Expand the schedule if you want to see principal, interest and remaining balance for every period. The computeInstallment helper is exported separately, so a product listing can call it directly to print "as low as … per month".
npx shadcn@latest add https://webberui.com/r/installment-calculator.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<InstallmentCalculator />
Installation
npx shadcn@latest add https://webberui.com/r/installment-calculator.jsonOr, once registries are configured in components.json, install it as @webberui/installment-calculator.
Usage
import {
InstallmentCalculator,
computeInstallment,
} from "@/components/ui/installment-calculator";
<InstallmentCalculator
price={36900}
plans={[
{ months: 3, annualRate: 0 },
{ months: 6, annualRate: 0, badge: "熱門" },
{ months: 12, annualRate: 4.99 },
{ months: 24, annualRate: 7.99, fee: 300 },
]}
defaultMonths={6}
minAmount={3000}
onChange={(plan, result) =>
console.log(`${plan.months} months, NT$${result.monthly} per month`)
}
/>
// The helper can be used on its own (e.g. "as low as … per month" in a product listing)
const { monthly, effectiveAnnualRate } = computeInstallment(36900, 12, 4.99);Props
| Prop | Type | Default | Description |
|---|---|---|---|
price | number | — | Product price |
plans | InstallmentPlan[] | 3 / 6 / 12 / 24 months | List of installment plans; the defaults are 3 and 6 months at 0%, 12 months at 4.5% and 24 months at 6.5% |
value | number | — | Controlled: the currently selected term in months (must exist in plans) |
defaultMonths | number | first plan | Uncontrolled: the initially selected term; falls back to the first plan when it is not in plans |
onChange | (plan: InstallmentPlan, result: InstallmentResult) => void | — | Callback when the user switches terms, with the selected plan and its computed result |
currency | string | "NT$" | Currency prefix |
showSchedule | boolean | true | Whether to offer the expandable per-period schedule |
minAmount | number | 0 | Minimum amount eligible for installments; when price is below it a notice is shown instead and the plan switch is disabled |
disclaimer | string | "實際費率以發卡銀行為準" (actual rates are set by the issuing bank) | Small-print disclaimer at the bottom; pass an empty string to hide it |
className | string | — | Forwarded to the outermost container |
InstallmentPlan
| Field | Type | Default | Description |
|---|---|---|---|
months | number | — | Term in months |
annualRate | number | — | Annual interest rate (%); 0 means zero interest |
fee | number | 0 | One-off handling fee |
label | string | "N 期" (N months) | Text on the segmented button |
badge | string | — | Small badge text at the top-right of the segmented button, e.g. "熱門" (popular) |
InstallmentResult
| Field | Type | Default | Description |
|---|---|---|---|
months | number | — | Term in months |
annualRate | number | — | Annual interest rate (%) |
fee | number | — | Handling fee |
monthly | number | — | Monthly payment (rounded to the dollar; the last period may differ slightly because it absorbs the rounding remainder) |
total | number | — | Total paid (sum of all payments + fee) |
interest | number | — | Total interest |
extraCost | number | — | Interest + fee |
effectiveAnnualRate | number | — | Effective annual rate (%, fee included, two decimals) |
zeroInterest | boolean | — | Whether the plan is zero-interest |
schedule | InstallmentScheduleRow[] | — | Per-period schedule |
InstallmentScheduleRow
| Field | Type | Default | Description |
|---|---|---|---|
period | number | — | Period number (starting at 1) |
payment | number | — | Amount due this period (principal + interest) |
principal | number | — | Principal repaid this period |
interest | number | — | Interest for this period |
balance | number | — | Remaining principal after this period |
computeInstallment(price, months, annualRate, fee?) is also a named export returning an InstallmentResult, so the same math can be reused in listings, the cart or checkout.
How it works
- Formula: monthly rate
r = annualRate / 12 / 100; whenris 0 the monthly payment isprice / n, otherwise the standard amortized paymentprice × r × (1 + r)^n / ((1 + r)^n − 1). Amounts are rounded to the dollar. - Schedule: interest for each period
= remaining principal × r, principal= monthly payment − interest; the last period clears whatever principal is left and absorbs the rounding remainder, so the principal parts add up exactly to the product price. - Effective annual rate: the fee is treated as a cost paid at the moment of borrowing (net financed amount
= price − fee), the monthly internal rate of return is solved by bisection and multiplied by 12 — with no fee it equals the nominal annual rate; a zero-interest plan with a fee shows an effective rate above 0, so the "0 利率" badge cannot mislead. - Rolling digits: each digit is a vertical 0–9 wheel that
translateYs to its target; keys are assigned from the right, so the ones digit keeps its wheel when the digit count changes and new digits fade in from the left. The first render outputs the final position directly, so hydration does not roll up from 0. - Threshold: when
priceis belowminAmount, the result card is replaced by a "below the installment threshold" notice and the plan switch is disabled.
Accessibility
- The term switch uses
role="radiogroup"withrole="radio"/aria-checked; Tab lands only on the selected item, arrow keys and Home / End move between plans and select them, and focus is shown with afocus-visiblering - The rolling-digit layer of the monthly amount is
aria-hiddenand the wrapper carries the full amount asaria-label; a hiddenaria-live="polite"one-line summary (term, monthly payment, total) is read out on switch, instead of turning the whole result card into a noisy live region - The schedule toggle carries
aria-expanded/aria-controls; the schedule is a nativetablewith acaptionandscope-marked column and row headers - When the user has "reduce motion" enabled at the system level, the wheels become static digits and the pill slide, badge pop, schedule expansion and value transitions all snap into place, leaving only the color and text feedback
Convenience Store Pickup
Three-step convenience-store pickup picker: chain → city/district → store list with search, hours and a selected-store summary card; brand-neutral, data supplied by props
Free Shipping Meter (React)
A multi-tier threshold meter (free shipping → gift → discount) for React: rolling 'NT$X more for free shipping' copy, a colour change across the whole bar and a one-shot micro-confetti burst when a tier is crossed, and reached tiers listed as chips — drop it into a cart header on its own.