Componentpickers0.87 KB gzipZero dependencies

Masked Currency

Formatted currency price input with prefix badge and numeric mask.

Preview

Live Preview
$
Live Stub Balance: $85.00

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add masked-currency

Or install the full npm package:

npm install bigbullui
import { MaskedCurrency } from "@/components/ui/masked-currency";

<MaskedCurrency currency="$" defaultValue={4500} onValueChange={(cents) => console.log(cents)} />

Usage guidance

Use Masked Currency when a ticket stub surface needs Formatted currency price input with prefix badge and numeric mask. Keep props minimal and prefer composition with sibling stubs.

Avoid Masked Currency for long form flows or dense data grids where a dedicated layout block fits better.

Source code

masked-currency.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface MaskedCurrencyProps
  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
  currency?: string;
  value?: number;
  defaultValue?: number;
  onValueChange?: (cents: number, formatted: string) => void;
  className?: string;
}

export function MaskedCurrency({
  currency = "$",
  value: controlledValue,
  defaultValue = 4500, // in cents (e.g. 4500 = $45.00)
  onValueChange,
  className,
  ...props
}: MaskedCurrencyProps) {
  const [internalCents, setInternalCents] = React.useState(defaultValue);
  const isControlled = controlledValue !== undefined;
  const currentCents = isControlled ? controlledValue : internalCents;

  const formatCents = (cents: number) => {
    const dollars = (cents / 100).toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    });
    return dollars;
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const rawDigits = e.target.value.replace(/[^0-9]/g, "");
    const cents = parseInt(rawDigits || "0", 10);
    if (!isControlled) setInternalCents(cents);
    onValueChange?.(cents, `${currency} ${formatCents(cents)}`);
  };

  return (
    <div
      className={cn(
        "relative inline-flex items-center rounded-lg border-2 border-foreground bg-card shadow-xs font-mono select-none",
        className
      )}
    >
      <span className="flex h-full items-center border-r-2 border-dashed border-border bg-secondary/70 px-3 text-xs font-bold text-muted-foreground">
        {currency}
      </span>
      <input
        type="text"
        inputMode="numeric"
        value={formatCents(currentCents)}
        onChange={handleChange}
        className={cn(
          "w-full bg-transparent px-3 py-2 text-right text-xs font-bold text-foreground outline-none",
          "focus-visible:ring-1 focus-visible:ring-accent"
        )}
        {...props}
      />
    </div>
  );
}

Props

PropTypeDescription
currency?string
value?number
defaultValue?number
onValueChange?(cents: number, formatted: string) => void
className?string