Display

Formatted Number

Locale-aware number, currency, and percentage formatting via Intl.NumberFormat. Reads the active locale from react-aria's I18nProvider, with formatter helpers for string contexts.

numbercurrencypercentageformatintli18nlocale

Grouped number

Locale-specific digit grouping. Compare en-US (comma) with de-DE (period).

en-US1,234,567.89
de-DE1.234.567,89

Currency

FormattedCurrency renders EUR with two decimals by default; override via currency.

EUR · de-DE1.499,50 €
USD · en-US$1,499.50
GBP · en-GB£1,499.50

Percentage

2 decimals12.34%
0 decimals42%

Custom options

Pass any Intl.NumberFormat options directly.

Compact9.8M
Unit88 km/h
Sign always+2.5

Children render prop

Wrap the formatted string to apply quebi tokens such as the brand teal.

24.990,00 €

Invalid value

A non-numeric value falls back to an em dash placeholder.

-

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import type { ReactNode } from "react"
import { useCallback } from "react"
import { useLocale } from "react-aria-components"

/**
 * FormattedNumber — quebi design system
 *
 * Locale-aware number formatting built on `Intl.NumberFormat`. Reads the active
 * locale from the nearest react-aria `I18nProvider` (override with the `locale`
 * prop). This is a headless formatter: it renders a bare `<span>` so it inherits
 * surrounding quebi typography — apply tokens via `className` or the `children`
 * render prop.
 */

export interface FormattedNumberProps {
  /**
   * The number value to format.
   */
  value: number | string
  /**
   * Locale for formatting. Defaults to the locale from the nearest I18nProvider.
   */
  locale?: string
  /**
   * Intl.NumberFormat options.
   */
  options?: Intl.NumberFormatOptions
  /**
   * Optional className applied to the default `<span>`.
   */
  className?: string
  /**
   * Custom children function for advanced formatting.
   */
  children?: (formattedValue: string) => ReactNode
}

/**
 * Format a plain number using locale-specific grouping (e.g. 1,024 in EN, 1.024 in DE).
 * Useful in string contexts (e.g. callback functions) where a React component cannot be used.
 */
export function formatNumber(value: number, locale: string): string {
  return new Intl.NumberFormat(locale, {
    maximumFractionDigits: 0,
  }).format(value)
}

/**
 * Hook that returns a locale-aware number formatting function.
 * Reads locale from the nearest I18nProvider automatically.
 */
export function useFormatNumber() {
  const { locale } = useLocale()
  return useCallback((value: number) => formatNumber(value, locale), [locale])
}

/**
 * Format a number as EUR currency string with always 2 decimal places.
 * Useful in string contexts (e.g. callback functions) where a React component cannot be used.
 */
export function formatCurrency(value: number, locale: string): string {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency: "EUR",
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(value)
}

/**
 * A component for formatting numbers using Intl.NumberFormat.
 * Reads locale from the nearest I18nProvider (react-aria-components) when no locale prop is given.
 *
 * @example
 * // Format as currency
 * <FormattedNumber value={123.45} options={{ style: 'currency', currency: 'EUR' }} />
 *
 * @example
 * // Format as percentage
 * <FormattedNumber value={0.1234} options={{ style: 'percent', minimumFractionDigits: 2 }} />
 *
 * @example
 * // Custom formatting with children
 * <FormattedNumber value={123.45} options={{ style: 'currency', currency: 'EUR' }}>
 *   {(formatted) => <span className="font-semibold text-quebi-brand">{formatted}</span>}
 * </FormattedNumber>
 */
export function FormattedNumber({
  value,
  locale,
  options = {},
  className,
  children,
}: FormattedNumberProps) {
  const { locale: racLocale } = useLocale()
  const resolvedLocale = locale ?? racLocale

  const numericValue = typeof value === "string" ? parseFloat(value) : value

  if (Number.isNaN(numericValue)) {
    return <span className={className}>-</span>
  }

  const formatter = new Intl.NumberFormat(resolvedLocale, options)
  const formattedValue = formatter.format(numericValue)

  if (children) {
    return <>{children(formattedValue)}</>
  }

  return <span className={className}>{formattedValue}</span>
}

/**
 * Format a number as currency (EUR by default), always with 2 decimal places.
 * Reads locale from the nearest I18nProvider automatically.
 */
export function FormattedCurrency({
  value,
  currency = "EUR",
  locale,
  ...props
}: Omit<FormattedNumberProps, "options"> & { currency?: string }) {
  const { locale: racLocale } = useLocale()
  const resolvedLocale = locale ?? racLocale

  return (
    <FormattedNumber
      value={value}
      locale={resolvedLocale}
      options={{
        style: "currency",
        currency,
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      }}
      {...props}
    />
  )
}

/**
 * Format a number as a percentage. Reads locale from the nearest I18nProvider automatically.
 */
export function FormattedPercentage({
  value,
  minimumFractionDigits = 2,
  locale,
  ...props
}: Omit<FormattedNumberProps, "options"> & { minimumFractionDigits?: number }) {
  const { locale: racLocale } = useLocale()
  const resolvedLocale = locale ?? racLocale

  return (
    <FormattedNumber
      value={value}
      locale={resolvedLocale}
      options={{ style: "percent", minimumFractionDigits }}
      {...props}
    />
  )
}