diff --git a/projects/core/eslint.config.js b/projects/core/eslint.config.js
index 3273083c7..c722b12f4 100644
--- a/projects/core/eslint.config.js
+++ b/projects/core/eslint.config.js
@@ -30,7 +30,11 @@ export default [
}
},
{
- files: ['src/format-datetime/format-datetime.ts', 'src/format-relative-time/format-relative-time.ts'],
+ files: [
+ 'src/format-datetime/format-datetime.ts',
+ 'src/format-number/format-number.ts',
+ 'src/format-relative-time/format-relative-time.ts'
+ ],
rules: {
'local/require-test-completeness': ['error', { skipSuffixes: ['.test.visual.ts'] }]
}
diff --git a/projects/core/package.json b/projects/core/package.json
index 84f7310ad..e4002a141 100644
--- a/projects/core/package.json
+++ b/projects/core/package.json
@@ -381,6 +381,18 @@
"types": "./dist/format-datetime/define.d.ts",
"default": "./dist/format-datetime/define.js"
},
+ "./format-number": {
+ "types": "./dist/format-number/index.d.ts",
+ "default": "./dist/format-number/index.js"
+ },
+ "./format-number/index.js": {
+ "types": "./dist/format-number/index.d.ts",
+ "default": "./dist/format-number/index.js"
+ },
+ "./format-number/define.js": {
+ "types": "./dist/format-number/define.d.ts",
+ "default": "./dist/format-number/define.js"
+ },
"./format-relative-time": {
"types": "./dist/format-relative-time/index.d.ts",
"default": "./dist/format-relative-time/index.js"
diff --git a/projects/core/src/bundle.ts b/projects/core/src/bundle.ts
index c35b589bb..7c1b320df 100644
--- a/projects/core/src/bundle.ts
+++ b/projects/core/src/bundle.ts
@@ -28,6 +28,7 @@ import '@nvidia-elements/core/dropdown-group/define.js';
import '@nvidia-elements/core/dropzone/define.js';
import '@nvidia-elements/core/file/define.js';
import '@nvidia-elements/core/format-datetime/define.js';
+import '@nvidia-elements/core/format-number/define.js';
import '@nvidia-elements/core/format-relative-time/define.js';
import '@nvidia-elements/core/forms/define.js';
import '@nvidia-elements/core/grid/define.js';
@@ -81,6 +82,7 @@ export * from '@nvidia-elements/core/dropdown';
export * from '@nvidia-elements/core/dropzone';
export * from '@nvidia-elements/core/file';
export * from '@nvidia-elements/core/format-datetime';
+export * from '@nvidia-elements/core/format-number';
export * from '@nvidia-elements/core/format-relative-time';
export * from '@nvidia-elements/core/forms';
export * from '@nvidia-elements/core/grid';
diff --git a/projects/core/src/format-number/define.ts b/projects/core/src/format-number/define.ts
new file mode 100644
index 000000000..074bb3d77
--- /dev/null
+++ b/projects/core/src/format-number/define.ts
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { define } from '@nvidia-elements/core/internal';
+import { FormatNumber } from '@nvidia-elements/core/format-number';
+
+define(FormatNumber);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'nve-format-number': FormatNumber;
+ }
+}
diff --git a/projects/core/src/format-number/format-number.css b/projects/core/src/format-number/format-number.css
new file mode 100644
index 000000000..5843acc02
--- /dev/null
+++ b/projects/core/src/format-number/format-number.css
@@ -0,0 +1,14 @@
+/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */
+/* SPDX-License-Identifier: Apache-2.0 */
+
+:host {
+ display: inline;
+}
+
+[internal-host] {
+ color: var(--nve-sys-text-color, inherit);
+}
+
+slot {
+ display: none;
+}
diff --git a/projects/core/src/format-number/format-number.examples.ts b/projects/core/src/format-number/format-number.examples.ts
new file mode 100644
index 000000000..5990217c8
--- /dev/null
+++ b/projects/core/src/format-number/format-number.examples.ts
@@ -0,0 +1,120 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import '@nvidia-elements/core/format-number/define.js';
+
+export default {
+ title: 'Elements/FormatNumber',
+ component: 'nve-format-number'
+};
+
+/**
+ * @summary Basic decimal formatting with localized grouping separators. Use for inline counts and metrics.
+ */
+export const Default = {
+ render: () => html`
+ 1234567.89
+ `
+};
+
+/**
+ * @summary Currency formatting for monetary values with locale-aware symbols and separators. Use for prices, budgets, and financial totals.
+ */
+export const Currency = {
+ render: () => html`
+
+ 1234.56
+ 1234.56
+ 1234
+
+ `
+};
+
+/**
+ * @summary Percent formatting for ratios and completion values. Source values should already represent a fraction (such as 0.85 for 85 percent).
+ */
+export const Percent = {
+ render: () => html`
+
+ 0.85
+ 0.126
+
+ `
+};
+
+/**
+ * @summary Unit formatting for measurements and quantities. Use for distances, storage sizes, or other numeric labels that need a localized unit suffix.
+ */
+export const Unit = {
+ render: () => html`
+
+ 1234.56
+ 2048
+ 22
+
+ `
+};
+
+/**
+ * @summary Notation presets for scientific, engineering, and compact display. Use compact notation in dashboards or cards where space matters.
+ */
+export const Notation = {
+ render: () => html`
+
+ 1234567
+ 1234567
+ 1234567
+ 1234567
+
+ `
+};
+
+/**
+ * @summary Sign display options for controlling positive and negative indicators. Use 'always' for delta values or 'exceptZero' for change indicators.
+ */
+export const SignDisplay = {
+ render: () => html`
+
+ 42
+ -42
+ 0
+ -42
+
+ `
+};
+
+/**
+ * @summary Fraction digit control for tuning decimal precision. Use to enforce fixed decimal places in financial or scientific contexts.
+ */
+export const FractionDigits = {
+ render: () => html`
+
+ 1.5
+ 1.567
+ 3
+
+ `
+};
+
+/**
+ * @summary Explicit locale settings for internationalized number output. Use when the target audience locale differs from the document language or browser default.
+ */
+export const Locale = {
+ render: () => html`
+
+ 1234.56
+ 1234
+ 1234567.89
+
+ `
+};
+
+/**
+ * @summary Number attribute input for values supplied by JavaScript or bound data. By default, the component formats text content, which also serves as the SSR fallback, and `number` wins when both are present.
+ */
+export const NumberAttribute = {
+ render: () => html`
+
+ `
+};
\ No newline at end of file
diff --git a/projects/core/src/format-number/format-number.test.axe.ts b/projects/core/src/format-number/format-number.test.axe.ts
new file mode 100644
index 000000000..28454bebc
--- /dev/null
+++ b/projects/core/src/format-number/format-number.test.axe.ts
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import { describe, expect, it } from 'vitest';
+import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
+import { runAxe } from '@internals/testing/axe';
+import { FormatNumber } from '@nvidia-elements/core/format-number';
+import '@nvidia-elements/core/format-number/define.js';
+
+describe(FormatNumber.metadata.tag, () => {
+ it('should pass axe check', async () => {
+ const fixture = await createFixture(html`
+ 1234567
+ 1234.56
+ 1234.56
+ `);
+
+ try {
+ await elementIsStable(fixture.querySelector(FormatNumber.metadata.tag));
+ const results = await runAxe([FormatNumber.metadata.tag]);
+ expect(results.violations.length).toBe(0);
+ } finally {
+ removeFixture(fixture);
+ }
+ });
+});
diff --git a/projects/core/src/format-number/format-number.test.lighthouse.ts b/projects/core/src/format-number/format-number.test.lighthouse.ts
new file mode 100644
index 000000000..aa40cdf00
--- /dev/null
+++ b/projects/core/src/format-number/format-number.test.lighthouse.ts
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect, test, describe } from 'vitest';
+import { lighthouseRunner } from '@internals/vite';
+
+describe('format-number lighthouse report', () => {
+ test('format-number should meet lighthouse benchmarks', async () => {
+ const report = await lighthouseRunner.getReport('nve-format-number', /* html */`
+ 1234.56
+
+ `);
+
+ expect(report.scores.performance).toBe(100);
+ expect(report.scores.accessibility).toBe(100);
+ expect(report.scores.bestPractices).toBe(100);
+ expect(report.payload.javascript.kb).toBeLessThan(12);
+ });
+});
\ No newline at end of file
diff --git a/projects/core/src/format-number/format-number.test.ssr.ts b/projects/core/src/format-number/format-number.test.ssr.ts
new file mode 100644
index 000000000..33e212c58
--- /dev/null
+++ b/projects/core/src/format-number/format-number.test.ssr.ts
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import { describe, expect, it } from 'vitest';
+import { ssrRunner } from '@internals/vite';
+import { FormatNumber } from '@nvidia-elements/core/format-number';
+import '@nvidia-elements/core/format-number/define.js';
+
+describe(FormatNumber.metadata.tag, () => {
+ it('should pass baseline ssr check', async () => {
+ const result = await ssrRunner.render(
+ html`1234.56`
+ );
+ expect(result.includes('shadowroot="open"')).toBe(true);
+ expect(result.includes('nve-format-number')).toBe(true);
+ });
+});
diff --git a/projects/core/src/format-number/format-number.test.ts b/projects/core/src/format-number/format-number.test.ts
new file mode 100644
index 000000000..3bc2fd1e1
--- /dev/null
+++ b/projects/core/src/format-number/format-number.test.ts
@@ -0,0 +1,316 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import { describe, expect, it, beforeEach, afterEach, vi, type MockInstance } from 'vitest';
+import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
+import { LogService } from '@nvidia-elements/core/internal';
+import { FormatNumber } from '@nvidia-elements/core/format-number';
+import '@nvidia-elements/core/format-number/define.js';
+
+describe(FormatNumber.metadata.tag, () => {
+ let fixture: HTMLElement;
+ let element: FormatNumber;
+ let originalDocumentLang: string;
+
+ beforeEach(async () => {
+ originalDocumentLang = document.documentElement.lang;
+ fixture = await createFixture(html`1234567`);
+ element = fixture.querySelector(FormatNumber.metadata.tag);
+ await elementIsStable(element);
+ });
+
+ afterEach(() => {
+ document.documentElement.lang = originalDocumentLang;
+ removeFixture(fixture);
+ });
+
+ it('should define element', () => {
+ expect(customElements.get(FormatNumber.metadata.tag)).toBeDefined();
+ });
+
+ it('should render formatted number from slot content', async () => {
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1,234,567');
+ });
+
+ it('should render semantic data element with value attribute', async () => {
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data).toBeTruthy();
+ expect(data!.getAttribute('value')).toBe('1234567');
+ });
+
+ it('should use number attribute over slot content', async () => {
+ element.number = '9999';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.getAttribute('value')).toBe('9999');
+ expect(data!.textContent!.trim()).toBe('9,999');
+ });
+
+ it('should re-render when slot content changes', async () => {
+ element.textContent = '2048';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.getAttribute('value')).toBe('2048');
+ expect(data!.textContent!.trim()).toBe('2,048');
+ });
+
+ describe('format-style', () => {
+ it('should format as decimal by default', async () => {
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1,234,567');
+ });
+
+ it('should format as currency', async () => {
+ element.number = '1234.56';
+ element.formatStyle = 'currency';
+ element.currency = 'USD';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('$1,234.56');
+ });
+
+ it('should format as percent', async () => {
+ element.number = '0.85';
+ element.formatStyle = 'percent';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('85%');
+ });
+
+ it('should format as unit', async () => {
+ element.number = '12';
+ element.formatStyle = 'unit';
+ element.unit = 'kilometer';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toContain('km');
+ });
+ });
+
+ describe('currency options', () => {
+ it('should display currency code', async () => {
+ element.number = '1234.56';
+ element.formatStyle = 'currency';
+ element.currency = 'USD';
+ element.currencyDisplay = 'code';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toContain('USD');
+ });
+
+ it('should display currency name', async () => {
+ element.number = '1234.56';
+ element.formatStyle = 'currency';
+ element.currency = 'USD';
+ element.currencyDisplay = 'name';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toContain('US dollars');
+ });
+
+ it('should use accounting sign for negative values', async () => {
+ element.number = '-1234.56';
+ element.formatStyle = 'currency';
+ element.currency = 'USD';
+ element.currencySign = 'accounting';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('($1,234.56)');
+ });
+ });
+
+ describe('notation', () => {
+ it('should format with compact notation short', async () => {
+ element.number = '1234567';
+ element.notation = 'compact';
+ element.compactDisplay = 'short';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1.2M');
+ });
+
+ it('should format with compact notation long', async () => {
+ element.number = '1234567';
+ element.notation = 'compact';
+ element.compactDisplay = 'long';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toContain('million');
+ });
+
+ it('should format with scientific notation', async () => {
+ element.number = '1234567';
+ element.notation = 'scientific';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toContain('E');
+ });
+ });
+
+ describe('sign-display', () => {
+ it('should show sign always', async () => {
+ element.number = '12';
+ element.signDisplay = 'always';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('+12');
+ });
+
+ it('should hide sign with never', async () => {
+ element.number = '-12';
+ element.signDisplay = 'never';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('12');
+ });
+ });
+
+ describe('unit-display', () => {
+ it('should display unit long', async () => {
+ element.number = '12';
+ element.formatStyle = 'unit';
+ element.unit = 'kilometer';
+ element.unitDisplay = 'long';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toContain('kilometers');
+ });
+ });
+
+ describe('use-grouping', () => {
+ it('should suppress grouping with false', async () => {
+ element.number = '1234567';
+ element.useGrouping = 'false';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1234567');
+ });
+ });
+
+ describe('fraction digits', () => {
+ it('should pad with minimum-fraction-digits', async () => {
+ element.number = '1.5';
+ element.minimumFractionDigits = 4;
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1.5000');
+ });
+
+ it('should round with maximum-fraction-digits', async () => {
+ element.number = '1.567';
+ element.maximumFractionDigits = 0;
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('2');
+ });
+ });
+
+ describe('locale', () => {
+ it('should fall back to document lang when locale is omitted', async () => {
+ document.documentElement.lang = 'de-DE';
+ removeFixture(fixture);
+
+ fixture = await createFixture(html`1234.56`);
+ element = fixture.querySelector(FormatNumber.metadata.tag);
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1.234,56');
+ });
+
+ it('should format with explicit de-DE locale', async () => {
+ removeFixture(fixture);
+
+ fixture = await createFixture(html`1234.56`);
+ element = fixture.querySelector(FormatNumber.metadata.tag);
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1.234,56');
+ });
+ });
+
+ describe('fallback', () => {
+ let warnSpy: MockInstance;
+
+ beforeEach(() => {
+ warnSpy = vi.spyOn(LogService, 'warn').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ warnSpy.mockRestore();
+ });
+
+ it('should show raw string for invalid number input', async () => {
+ element.number = 'not-a-number';
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('not-a-number');
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid numeric value'));
+ });
+
+ it('should fall back for invalid Intl options', async () => {
+ element.number = '1234.56';
+ element.setAttribute('format-style', 'banana');
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1234.56');
+ expect(warnSpy).toHaveBeenCalled();
+ });
+
+ it('should fall back when currency is missing', async () => {
+ element.number = '1234.56';
+ element.formatStyle = 'currency';
+ element.currency = undefined;
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('1234.56');
+ expect(warnSpy).toHaveBeenCalled();
+ });
+
+ it('should fall back when unit is missing', async () => {
+ element.number = '12';
+ element.formatStyle = 'unit';
+ element.unit = undefined;
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('12');
+ expect(warnSpy).toHaveBeenCalled();
+ });
+
+ it('should render empty for no content', async () => {
+ removeFixture(fixture);
+
+ fixture = await createFixture(html``);
+ element = fixture.querySelector(FormatNumber.metadata.tag);
+ await elementIsStable(element);
+
+ const data = element.shadowRoot!.querySelector('data');
+ expect(data!.textContent!.trim()).toBe('');
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/projects/core/src/format-number/format-number.ts b/projects/core/src/format-number/format-number.ts
new file mode 100644
index 000000000..db433811f
--- /dev/null
+++ b/projects/core/src/format-number/format-number.ts
@@ -0,0 +1,190 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html, LitElement } from 'lit';
+import { property } from 'lit/decorators/property.js';
+import { useStyles, typeSSR, LogService } from '@nvidia-elements/core/internal';
+import styles from './format-number.css?inline';
+
+export type FormatNumberStyle = 'decimal' | 'currency' | 'percent' | 'unit';
+export type CurrencyDisplayOption = 'symbol' | 'code' | 'name' | 'narrowSymbol';
+export type CurrencySignOption = 'standard' | 'accounting';
+export type NotationOption = 'standard' | 'scientific' | 'engineering' | 'compact';
+export type CompactDisplayOption = 'short' | 'long';
+export type UnitDisplayOption = 'short' | 'long' | 'narrow';
+export type SignDisplayOption = 'auto' | 'never' | 'always' | 'exceptZero';
+
+const STRING_KEYS: [keyof FormatNumber, string][] = [
+ ['currency', 'currency'],
+ ['currencyDisplay', 'currencyDisplay'],
+ ['currencySign', 'currencySign'],
+ ['notation', 'notation'],
+ ['compactDisplay', 'compactDisplay'],
+ ['unit', 'unit'],
+ ['unitDisplay', 'unitDisplay'],
+ ['signDisplay', 'signDisplay']
+];
+
+const NUMBER_KEYS: [keyof FormatNumber, string][] = [
+ ['minimumFractionDigits', 'minimumFractionDigits'],
+ ['maximumFractionDigits', 'maximumFractionDigits'],
+ ['minimumIntegerDigits', 'minimumIntegerDigits']
+];
+
+/**
+ * @element nve-format-number
+ * @description A localized number formatter for currencies, percentages, units, and compact notation, backed by Intl.NumberFormat.
+ * Provide a `currency` attribute when `formatStyle` is `currency`, and a `unit` attribute when `formatStyle` is `unit`.
+ * @since 0.0.0
+ * @entrypoint \@nvidia-elements/core/format-number
+ * @slot - Numeric string to format (such as 1234567 or 1234.56). Serves as fallback before hydration.
+ */
+@typeSSR()
+export class FormatNumber extends LitElement {
+ static styles = useStyles([styles]);
+
+ static readonly metadata = {
+ tag: 'nve-format-number',
+ version: '0.0.0'
+ };
+
+ /**
+ * Optional numeric string for values supplied by JavaScript or bound data.
+ * By default, the component formats the element's text content, which also serves as the SSR fallback.
+ * When both are present, this property takes precedence.
+ */
+ @property({ type: String }) number?: string;
+
+ /**
+ * Language tag (such as en-US, de-DE). Defaults to document.documentElement.lang or browser default.
+ */
+ @property({ type: String }) locale?: string;
+
+ /**
+ * Formatting style: 'decimal' | 'currency' | 'percent' | 'unit'.
+ */
+ @property({ type: String, attribute: 'format-style' }) formatStyle: FormatNumberStyle = 'decimal';
+
+ /**
+ * ISO 4217 currency code (such as USD or EUR). Required when formatStyle is currency.
+ */
+ @property({ type: String }) currency?: string;
+
+ /**
+ * Currency sign style: 'standard' | 'accounting'.
+ */
+ @property({ type: String, attribute: 'currency-sign' }) currencySign?: CurrencySignOption;
+
+ /**
+ * Currency display style: 'symbol' | 'code' | 'name' | 'narrowSymbol'.
+ */
+ @property({ type: String, attribute: 'currency-display' }) currencyDisplay?: CurrencyDisplayOption;
+
+ /**
+ * Unit identifier (such as kilometer or byte). Required when formatStyle is unit.
+ */
+ @property({ type: String }) unit?: string;
+
+ /**
+ * Unit display style: 'short' | 'long' | 'narrow'.
+ */
+ @property({ type: String, attribute: 'unit-display' }) unitDisplay?: UnitDisplayOption;
+
+ /**
+ * Number notation: 'standard' | 'scientific' | 'engineering' | 'compact'.
+ */
+ @property({ type: String }) notation?: NotationOption;
+
+ /**
+ * Compact notation display: 'short' | 'long'. Only applies when notation is compact.
+ */
+ @property({ type: String, attribute: 'compact-display' }) compactDisplay?: CompactDisplayOption;
+
+ /**
+ * Sign display: 'auto' | 'never' | 'always' | 'exceptZero'.
+ */
+ @property({ type: String, attribute: 'sign-display' }) signDisplay?: SignDisplayOption;
+
+ /**
+ * Grouping separators: 'auto' | 'always' | 'min2' | 'true' | 'false'.
+ */
+ @property({ type: String, attribute: 'use-grouping' }) useGrouping?: string;
+
+ /**
+ * Pad fraction output to at least this many digits (0-20).
+ */
+ @property({ type: Number, attribute: 'minimum-fraction-digits' }) minimumFractionDigits?: number;
+
+ /**
+ * Round fraction output to at most this many digits (0-20).
+ */
+ @property({ type: Number, attribute: 'maximum-fraction-digits' }) maximumFractionDigits?: number;
+
+ /**
+ * Pad integer output to at least this many digits (1-21).
+ */
+ @property({ type: Number, attribute: 'minimum-integer-digits' }) minimumIntegerDigits?: number;
+
+ get #rawValue(): string {
+ return this.number ?? this.textContent?.trim() ?? '';
+ }
+
+ get #resolvedLocale(): string | undefined {
+ return this.locale ?? (globalThis.document?.documentElement?.lang || undefined);
+ }
+
+ get #parsedNumber(): number | null {
+ const raw = this.#rawValue;
+ if (!raw) return null;
+
+ const numericValue = Number(raw);
+ if (Number.isFinite(numericValue)) return numericValue;
+
+ LogService.warn(`format-number: invalid numeric value "${raw}"`);
+ return null;
+ }
+
+ get #formatOptions(): Intl.NumberFormatOptions {
+ const options: Intl.NumberFormatOptions = { style: this.formatStyle };
+
+ for (const [prop, key] of STRING_KEYS) {
+ const value = this[prop] as string | undefined;
+ if (value !== undefined) (options as Record)[key] = value;
+ }
+
+ for (const [prop, key] of NUMBER_KEYS) {
+ const value = this[prop] as number | undefined;
+ if (value !== undefined) (options as Record)[key] = value;
+ }
+
+ if (this.useGrouping !== undefined) {
+ const grouping = this.useGrouping === 'false' ? false : this.useGrouping === 'true' ? true : this.useGrouping;
+ (options as Record).useGrouping = grouping;
+ }
+
+ return options;
+ }
+
+ get #formattedNumber(): string {
+ const raw = this.#rawValue;
+ if (!raw) return '';
+
+ const numericValue = this.#parsedNumber;
+ if (numericValue === null) return raw;
+
+ try {
+ return new Intl.NumberFormat(this.#resolvedLocale, this.#formatOptions).format(numericValue);
+ } catch (e) {
+ LogService.warn(`format-number: ${(e as Error).message}`);
+ return raw;
+ }
+ }
+
+ render() {
+ return html`${this.#formattedNumber}`;
+ }
+
+ #onSlotChange() {
+ this.requestUpdate();
+ }
+}
diff --git a/projects/core/src/format-number/index.ts b/projects/core/src/format-number/index.ts
new file mode 100644
index 000000000..9c479cab6
--- /dev/null
+++ b/projects/core/src/format-number/index.ts
@@ -0,0 +1,4 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from './format-number.js';
diff --git a/projects/internals/eslint/src/configs/lit.js b/projects/internals/eslint/src/configs/lit.js
index f3ddf3d17..6969cbb2c 100644
--- a/projects/internals/eslint/src/configs/lit.js
+++ b/projects/internals/eslint/src/configs/lit.js
@@ -148,6 +148,7 @@ export const litConfig = [
'dropdown-group',
'progressive-filter-chip',
'format-datetime',
+ 'format-number',
'format-relative-time'
]
}
diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js
index ad35b98ea..28171c993 100644
--- a/projects/site/src/_11ty/layouts/common.js
+++ b/projects/site/src/_11ty/layouts/common.js
@@ -224,6 +224,7 @@ export const renderDocsNav = data => /* html */ `
Dropzone
File
Format Datetime
+ Format Number
Format Relative Time
Forms
diff --git a/projects/site/src/docs/elements/format-number.md b/projects/site/src/docs/elements/format-number.md
new file mode 100644
index 000000000..e4d08d2aa
--- /dev/null
+++ b/projects/site/src/docs/elements/format-number.md
@@ -0,0 +1,47 @@
+---
+{
+ title: 'Format Number',
+ layout: 'docs.11ty.js',
+ tag: 'nve-format-number'
+}
+---
+
+## Installation
+
+{% install 'nve-format-number' %}
+
+## Default
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'Default' %}
+
+## Currency
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'Currency' %}
+
+## Percent
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'Percent' %}
+
+## Unit
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'Unit' %}
+
+## Notation
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'Notation' %}
+
+## Sign Display
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'SignDisplay' %}
+
+## Fraction Digits
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'FractionDigits' %}
+
+## Locale
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'Locale' %}
+
+## Number Attribute
+
+{% example '@nvidia-elements/core/format-number/format-number.examples.json', 'NumberAttribute' %}