KKiotaPay UI
Browse documentation

Money Input

A currency amount field built on the shadcn Input. It reads and writes minor units — integer cents — so callers never handle floating-point money.

Installation

pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/money-input.json

Installs components/custom/money-input.tsx, plus the shadcn input primitive and kiota-theme if you do not already have them.

Usage

'use client'

import * as React from 'react'

import { MoneyInput } from '@/components/custom/money-input'

export function AmountField() {
  const [amount, setAmount] = React.useState<number | null>(null)

  return <MoneyInput value={amount} onValueChange={setAmount} placeholder="0.00" />
}

Why minor units

0.1 + 0.2 is 0.30000000000000004. That is not a quirk to work around; it is what binary floating point does to decimal fractions, and it compounds across a column of totals until a reconciliation report is off by a cent and nobody can say where.

Integers do not have that failure mode. 1230 is exactly 1230, and KSh 12.30 is what you render at the edge. So the component's value and onValueChange both speak minor units, and conversion happens only at display time.

import { formatMoney, toMinorUnits, fromMinorUnits } from '@/lib/kiota-format'

formatMoney(125050)              // "KSh 1,250.50"
formatMoney(125050, { currency: 'USD', locale: 'en-US' })  // "$1,250.50"
toMinorUnits(1250.5)             // 125050
fromMinorUnits(125050)           // 1250.5

Precision is resolved from CLDR at runtime rather than a hardcoded table, so it stays correct per currency: KES and USD get two decimals, JPY and UGX get none, KWD gets three.

Props

Extends React.ComponentProps<'input'> minus value, defaultValue, onChange and type.

PropTypeDefaultDescription
valuenumber | null—Amount in minor units. null renders an empty field.
onValueChange(minorUnits: number | null) => void—Fires on every keystroke with the parsed amount.
currencystring'KES'ISO 4217 code. Drives symbol, grouping and precision.
localestring'en-KE'BCP 47 tag used for grouping and symbol form.
showSymbolbooleantrueRender the currency symbol inside the field.
align'left' | 'right''left'Text alignment. Use 'right' inside tables.

Examples

Multiple currencies

Precision follows the currency without any configuration — yen has no decimal place to type into.

In a form

Validation compares minor units directly, so there is no rounding step between what the user typed and what you check.

Behaviour notes

Focused and blurred states differ deliberately. While focused the field shows a plain editable number; on blur it reformats with locale grouping. Editing a grouped string directly is the standard failure mode for currency inputs — the caret jumps whenever a separator is inserted mid-word — so the editable and display representations are kept separate.

Input parsing is permissive. 1,234.56 and 1.234,56 both parse: the last separator is treated as the decimal mark, which is correct under either convention. Unparseable input yields null rather than 0, so an empty field is distinguishable from a deliberate zero.

The symbol is decorative. It carries aria-hidden, and its width is measured in ch so KSh and $ both align without a per-currency lookup. Pair the field with a <Label> that names the currency when it matters.