diff --git a/projects/core/.visual/progress-gauge.dark.png b/projects/core/.visual/progress-gauge.dark.png
new file mode 100644
index 000000000..db5e281be
--- /dev/null
+++ b/projects/core/.visual/progress-gauge.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0236d6b78bf6e1c9b51a4c75e47da7532624294d1beb668a88ef905d5e4eb832
+size 50155
diff --git a/projects/core/.visual/progress-gauge.png b/projects/core/.visual/progress-gauge.png
new file mode 100644
index 000000000..e9330b117
--- /dev/null
+++ b/projects/core/.visual/progress-gauge.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b19ee9c236e3538b0c846171fc8820514b5816b4975bef61d3ea72a09ee860c1
+size 48192
diff --git a/projects/core/package.json b/projects/core/package.json
index d1e78788b..5a78bf857 100644
--- a/projects/core/package.json
+++ b/projects/core/package.json
@@ -605,6 +605,18 @@
"types": "./dist/progress-bar/define.d.ts",
"default": "./dist/progress-bar/define.js"
},
+ "./progress-gauge": {
+ "types": "./dist/progress-gauge/index.d.ts",
+ "default": "./dist/progress-gauge/index.js"
+ },
+ "./progress-gauge/index.js": {
+ "types": "./dist/progress-gauge/index.d.ts",
+ "default": "./dist/progress-gauge/index.js"
+ },
+ "./progress-gauge/define.js": {
+ "types": "./dist/progress-gauge/define.d.ts",
+ "default": "./dist/progress-gauge/define.js"
+ },
"./progress-ring": {
"types": "./dist/progress-ring/index.d.ts",
"default": "./dist/progress-ring/index.js"
diff --git a/projects/core/src/bundle.ts b/projects/core/src/bundle.ts
index b94181262..ff6c31914 100644
--- a/projects/core/src/bundle.ts
+++ b/projects/core/src/bundle.ts
@@ -47,6 +47,7 @@ import '@nvidia-elements/core/panel/define.js';
import '@nvidia-elements/core/password/define.js';
import '@nvidia-elements/core/preferences-input/define.js';
import '@nvidia-elements/core/progress-bar/define.js';
+import '@nvidia-elements/core/progress-gauge/define.js';
import '@nvidia-elements/core/progress-ring/define.js';
import '@nvidia-elements/core/progressive-filter-chip/define.js';
import '@nvidia-elements/core/pulse/define.js';
@@ -115,6 +116,7 @@ export * from '@nvidia-elements/core/panel';
export * from '@nvidia-elements/core/password';
export * from '@nvidia-elements/core/preferences-input';
export * from '@nvidia-elements/core/progress-bar';
+export * from '@nvidia-elements/core/progress-gauge';
export * from '@nvidia-elements/core/progress-ring';
export * from '@nvidia-elements/core/progressive-filter-chip';
export * from '@nvidia-elements/core/pulse';
diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts
index a493fa164..4caf2e338 100644
--- a/projects/core/src/index.test.lighthouse.ts
+++ b/projects/core/src/index.test.lighthouse.ts
@@ -15,7 +15,7 @@ describe('lighthouse report', () => {
expect(report.scores.performance).toBe(100);
expect(report.scores.accessibility).toBe(100);
expect(report.scores.bestPractices).toBe(100);
- expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(130.5);
+ expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(132);
// if sudden drop in size, check vite bundle config and bundle demo to ensure side effects are properly preserved
expect(report.payload.javascript.requests['index.js'].kb).toBeGreaterThan(120);
@@ -62,6 +62,7 @@ describe('lighthouse report', () => {
import '@nvidia-elements/core/password/define.js';
import '@nvidia-elements/core/preferences-input/define.js';
import '@nvidia-elements/core/progress-bar/define.js';
+ import '@nvidia-elements/core/progress-gauge/define.js';
import '@nvidia-elements/core/progress-ring/define.js';
import '@nvidia-elements/core/progressive-filter-chip/define.js';
import '@nvidia-elements/core/radio/define.js';
diff --git a/projects/core/src/progress-gauge/define.ts b/projects/core/src/progress-gauge/define.ts
new file mode 100644
index 000000000..e875174c6
--- /dev/null
+++ b/projects/core/src/progress-gauge/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 { ProgressGauge } from '@nvidia-elements/core/progress-gauge';
+
+define(ProgressGauge);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'nve-progress-gauge': ProgressGauge;
+ }
+}
diff --git a/projects/core/src/progress-gauge/index.ts b/projects/core/src/progress-gauge/index.ts
new file mode 100644
index 000000000..af2979efa
--- /dev/null
+++ b/projects/core/src/progress-gauge/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 './progress-gauge.js';
diff --git a/projects/core/src/progress-gauge/progress-gauge.css b/projects/core/src/progress-gauge/progress-gauge.css
new file mode 100644
index 000000000..04f20b580
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.css
@@ -0,0 +1,134 @@
+/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */
+/* SPDX-License-Identifier: Apache-2.0 */
+
+:host {
+ --color: var(--nve-sys-text-emphasis-color);
+ --background: var(--nve-sys-interaction-background);
+ --accent-color: var(--nve-sys-interaction-color);
+ --gauge-width: 12px;
+ --font-size: var(--nve-ref-font-size-400);
+ --gap: var(--nve-ref-space-xs);
+ --width: 128px;
+ --height: var(--width);
+ --_animation-duration: var(--nve-ref-animation-duration-250);
+
+ display: inline-block;
+ position: relative;
+ width: var(--width);
+ height: var(--height);
+ container-type: inline-size;
+ contain: content;
+ text-box: trim-both cap alphabetic;
+}
+
+:host([size='sm']) {
+ --font-size: var(--nve-ref-font-size-300);
+ --width: 96px;
+}
+
+:host([size='lg']) {
+ --font-size: var(--nve-ref-font-size-500);
+ --width: 160px;
+}
+
+[internal-host] {
+ display: grid;
+ place-items: center;
+ position: relative;
+ height: 100%;
+}
+
+:host([container='half']) {
+ --height: calc(var(--width) / 2);
+}
+
+:host([container='half']) [internal-host] {
+ place-items: end center;
+}
+
+svg {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ overflow: visible;
+}
+
+path {
+ fill: none;
+ stroke-linecap: round;
+ stroke-width: var(--gauge-width);
+}
+
+path.background {
+ stroke: var(--background);
+}
+
+path.gauge {
+ animation: gauge-progress-in var(--_animation-duration) var(--nve-ref-animation-easing-100);
+ stroke: var(--accent-color);
+ stroke-dasharray: var(--_progress) 100;
+ transition: stroke-dasharray var(--_animation-duration) var(--nve-ref-animation-easing-100);
+ will-change: stroke-dasharray;
+}
+
+path.gauge[empty] {
+ animation: none;
+ stroke-linecap: butt;
+}
+
+slot {
+ display: flex;
+ flex-direction: column;
+ place-items: center;
+ justify-content: center;
+ height: 100%;
+ gap: var(--gap);
+ color: var(--color);
+ font-size: var(--font-size);
+ font-weight: var(--nve-ref-font-weight-medium);
+}
+
+:host([container='half']) slot {
+ transform: translateY(10cqw);
+}
+
+::slotted(*) {
+ color: var(--color);
+ font-size: var(--font-size);
+}
+
+:host([status='success']) {
+ --accent-color: var(--nve-sys-support-success-emphasis-color);
+ --background: var(--nve-sys-support-success-muted-color);
+}
+
+:host([status='warning']) {
+ --accent-color: var(--nve-sys-support-warning-emphasis-color);
+ --background: var(--nve-sys-support-warning-muted-color);
+}
+
+:host([status='danger']) {
+ --accent-color: var(--nve-sys-support-danger-emphasis-color);
+ --background: var(--nve-sys-support-danger-muted-color);
+}
+
+:host([status='accent']) {
+ --accent-color: var(--nve-sys-accent-secondary-background);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ :host {
+ --_animation-duration: 0s;
+ }
+}
+
+@keyframes gauge-progress-in {
+ from {
+ stroke-dasharray: 0 100;
+ }
+
+ to {
+ stroke-dasharray: var(--_progress) 100;
+ }
+}
diff --git a/projects/core/src/progress-gauge/progress-gauge.examples.ts b/projects/core/src/progress-gauge/progress-gauge.examples.ts
new file mode 100644
index 000000000..65c5060d5
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.examples.ts
@@ -0,0 +1,136 @@
+// 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/progress-gauge/define.js';
+
+export default {
+ title: 'Elements/Progress Gauge',
+ component: 'nve-progress-gauge',
+};
+
+/**
+ * @summary 270-degree progress gauges for displaying system resource usage.
+ */
+export const Default = {
+ render: () => html`
+
+ 50%
+ 66%
+
+`};
+
+/**
+ * @summary Container variants compare the default 270-degree gauge with the compact half gauge for telemetry layouts with tighter vertical space.
+ * @tags test-case
+ */
+export const Container = {
+ render: () => html`
+
+ 66%
+ 66%
+
+`};
+
+/**
+ * @summary Gauges with values from 0% to 100% for displaying system resource usage.
+ * @tags test-case
+ */
+export const Values = {
+ render: () => html`
+
+ 0%
+ 33%
+ 66%
+ 100%
+
+`};
+
+/**
+ * @summary Progress gauges with custom max values for mission checkpoints, validation clips, and map tile processing.
+ * @tags test-case
+ */
+export const Max = {
+ render: () => html`
+
+ 5/20
+ 10/20
+ 15/20
+
+`};
+
+/**
+ * @summary Progress gauges with accent, success, warning, and danger colors for autonomous system health and readiness signals.
+ * @tags test-case
+ */
+export const Status = {
+ render: () => html`
+
+ 50%
+ 75%
+ 75%
+ 2.1m
+ 0Hz
+
+`};
+
+/**
+ * @summary Small progress gauge paired with route-solve text for compact autonomous vehicle task rows.
+ * @tags test-case
+ */
+export const WithText = {
+ render: () => html`
+
+ 2.4s
+ Route solve
+
+`};
+
+/**
+ * @summary Progress gauges in small, medium, and large sizes for dense robotics and autonomous vehicle dashboards.
+ * @tags test-case
+ */
+export const Sizing = {
+ render: () => html`
+
+ 30Hz
+ 12Hz
+ 84%
+
+`};
+
+/**
+ * @summary Use for displaying real-time system load and performance metrics.
+ * @tags pattern
+ */
+export const Dynamic = {
+ render: () => html`
+
+
+`};
diff --git a/projects/core/src/progress-gauge/progress-gauge.test.axe.ts b/projects/core/src/progress-gauge/progress-gauge.test.axe.ts
new file mode 100644
index 000000000..a66db58c8
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.test.axe.ts
@@ -0,0 +1,33 @@
+// 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 } from 'vitest';
+import { createFixture, removeFixture, elementIsStable } from '@internals/testing';
+import { runAxe } from '@internals/testing/axe';
+import { ProgressGauge } from '@nvidia-elements/core/progress-gauge';
+import '@nvidia-elements/core/progress-gauge/define.js';
+
+describe(ProgressGauge.metadata.tag, () => {
+ let fixture: HTMLElement;
+
+ beforeEach(async () => {
+ fixture = await createFixture(html`
+
+
+
+
+ `);
+ const elements = Array.from(fixture.querySelectorAll(ProgressGauge.metadata.tag)) as ProgressGauge[];
+ await Promise.all(elements.map(gauge => elementIsStable(gauge)));
+ });
+
+ afterEach(() => {
+ removeFixture(fixture);
+ });
+
+ it('should pass axe check', async () => {
+ const results = await runAxe([ProgressGauge.metadata.tag]);
+ expect(results.violations.length).toBe(0);
+ });
+});
diff --git a/projects/core/src/progress-gauge/progress-gauge.test.lighthouse.ts b/projects/core/src/progress-gauge/progress-gauge.test.lighthouse.ts
new file mode 100644
index 000000000..2191bc5d7
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.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('progress-gauge lighthouse report', () => {
+ test('progress-gauge should meet lighthouse benchmarks', async () => {
+ const report = await lighthouseRunner.getReport('nve-progress-gauge', /* html */`
+
+
+ `);
+
+ expect(report.scores.performance).toBe(100);
+ expect(report.scores.accessibility).toBe(100);
+ expect(report.scores.bestPractices).toBe(100);
+ expect(report.payload.javascript.kb).toBeLessThan(15);
+ });
+});
diff --git a/projects/core/src/progress-gauge/progress-gauge.test.ssr.ts b/projects/core/src/progress-gauge/progress-gauge.test.ssr.ts
new file mode 100644
index 000000000..c0541be94
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.test.ssr.ts
@@ -0,0 +1,16 @@
+// 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 { ProgressGauge } from '@nvidia-elements/core/progress-gauge';
+import '@nvidia-elements/core/progress-gauge/define.js';
+
+describe(ProgressGauge.metadata.tag, () => {
+ it('should pass baseline ssr check', async () => {
+ const result = await ssrRunner.render(html``);
+ expect(result.includes('shadowroot="open"')).toBe(true);
+ expect(result.includes('nve-progress-gauge')).toBe(true);
+ });
+});
diff --git a/projects/core/src/progress-gauge/progress-gauge.test.ts b/projects/core/src/progress-gauge/progress-gauge.test.ts
new file mode 100644
index 000000000..9b9f0d00d
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.test.ts
@@ -0,0 +1,268 @@
+// 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 } from 'vitest';
+import { createFixture, removeFixture, elementIsStable } from '@internals/testing';
+import { ProgressGauge } from '@nvidia-elements/core/progress-gauge';
+import '@nvidia-elements/core/progress-gauge/define.js';
+
+describe(ProgressGauge.metadata.tag, () => {
+ let fixture: HTMLElement;
+ let element: ProgressGauge;
+
+ beforeEach(async () => {
+ fixture = await createFixture(html`
+
+ `);
+ element = fixture.querySelector(ProgressGauge.metadata.tag);
+ await elementIsStable(element);
+ });
+
+ afterEach(() => {
+ removeFixture(fixture);
+ });
+
+ it('should define element', () => {
+ expect(customElements.get(ProgressGauge.metadata.tag)).toBeDefined();
+ });
+
+ it('should set aria attributes', async () => {
+ element.value = 50;
+ element.max = 80;
+ await elementIsStable(element);
+
+ expect(element._internals.role).toBe('progressbar');
+ expect(element._internals.ariaValueNow).toBe('50');
+ expect(element._internals.ariaValueMax).toBe('80');
+ expect(element._internals.ariaLabel).toBe('information');
+
+ element.status = 'success';
+ await elementIsStable(element);
+ expect(element._internals.ariaLabel).toBe('success');
+ });
+
+ it('should default to neutral status', () => {
+ expect(element.status).toBe('neutral');
+ });
+
+ it('should default to the 270-degree container', () => {
+ expect(element.container).toBeUndefined();
+ });
+
+ it('should default max to 100', () => {
+ expect(element.max).toBe(100);
+ });
+
+ it('should default value to 0', () => {
+ expect(element.value).toBe(0);
+ });
+
+ it('should default gauge width to 12px', () => {
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+
+ expect(getComputedStyle(gauge).strokeWidth).toBe('12px');
+ });
+
+ it.each([
+ ['default', undefined, '128px'],
+ ['sm', 'sm', '96px'],
+ ['md', 'md', '128px'],
+ ['lg', 'lg', '160px']
+ ] as const)('should set %s width', async (_, size, width) => {
+ if (size) {
+ element.size = size;
+ }
+
+ await elementIsStable(element);
+
+ expect(getComputedStyle(element).width).toBe(width);
+ });
+
+ it('should scale slotted text with size', async () => {
+ removeFixture(fixture);
+ fixture = await createFixture(html`
+
+ 50%
+ 50%
+ 50%
+
+ `);
+
+ const gauges = Array.from(fixture.querySelectorAll(ProgressGauge.metadata.tag)) as ProgressGauge[];
+ await Promise.all(gauges.map(gauge => elementIsStable(gauge)));
+
+ const fontSizes = ['sm', 'md', 'lg'].map(size =>
+ parseFloat(getComputedStyle(fixture.querySelector(`[data-size="${size}"]`) as HTMLElement).fontSize)
+ );
+
+ expect(fontSizes[0]).toBeLessThan(fontSizes[1]);
+ expect(fontSizes[1]).toBeLessThan(fontSizes[2]);
+ });
+
+ it('should not apply the status color to slotted text', async () => {
+ removeFixture(fixture);
+ fixture = await createFixture(html`
+
+ 50%
+
+ `);
+ element = fixture.querySelector(ProgressGauge.metadata.tag);
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+ const text = fixture.querySelector('span') as HTMLSpanElement;
+
+ expect(getComputedStyle(text).color).not.toBe(getComputedStyle(gauge).stroke);
+ });
+
+ it.each(['warning', 'success', 'danger'] as const)(
+ 'should leave the default slot empty for %s status',
+ async status => {
+ element.status = status;
+ await elementIsStable(element);
+
+ const defaultSlot = element.shadowRoot.querySelector('slot:not([name])') as HTMLSlotElement;
+ expect(defaultSlot.childElementCount).toBe(0);
+ expect(defaultSlot.textContent.trim()).toBe('');
+ }
+ );
+
+ it('should render default state as determinate 0 progress', async () => {
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+
+ expect(gauge.getAttribute('stroke-dasharray')).toBe('0 100');
+ });
+
+ it('should suppress rounded foreground caps when progress is 0', async () => {
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+
+ expect(gauge.hasAttribute('empty')).toBe(true);
+ expect(getComputedStyle(gauge).strokeLinecap).toBe('butt');
+
+ element.value = 1;
+ await elementIsStable(element);
+
+ expect(gauge.hasAttribute('empty')).toBe(false);
+ expect(getComputedStyle(gauge).strokeLinecap).toBe('round');
+ });
+
+ it('should animate progress on initial render and value changes', async () => {
+ removeFixture(fixture);
+ fixture = await createFixture(html`
+
+ `);
+ element = fixture.querySelector(ProgressGauge.metadata.tag);
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+
+ expect(gauge.style.getPropertyValue('--_progress')).toBe('50');
+ expect(getComputedStyle(gauge).animationName).toBe('gauge-progress-in');
+ expect(getComputedStyle(gauge).transitionProperty).toBe('stroke-dasharray');
+
+ element.value = 75;
+ await elementIsStable(element);
+
+ expect(gauge.style.getPropertyValue('--_progress')).toBe('75');
+ expect(gauge.getAttribute('stroke-dasharray')).toBe('75 100');
+ });
+
+ it('should render a 270-degree inset arc with rounded ends by default', async () => {
+ element.value = 50;
+ await elementIsStable(element);
+
+ const svg = element.shadowRoot.querySelector('svg') as SVGElement;
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+
+ expect(svg.getAttribute('viewBox')).toBe('0 0 128 128');
+ expect(gauge.getAttribute('d')).toBe('M 27.23 100.77 A 52 52 0 1 1 100.77 100.77');
+ expect(getComputedStyle(gauge).strokeLinecap).toBe('round');
+ expect(getComputedStyle(element).height).toBe('128px');
+ });
+
+ it('should render the half container with the semi-circular arc', async () => {
+ element.container = 'half';
+ element.value = 50;
+ await elementIsStable(element);
+
+ const svg = element.shadowRoot.querySelector('svg') as SVGElement;
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+
+ expect(element.getAttribute('container')).toBe('half');
+ expect(svg.getAttribute('viewBox')).toBe('0 0 128 64');
+ expect(gauge.getAttribute('d')).toBe('M 12 64 A 52 52 0 0 1 116 64');
+ expect(getComputedStyle(gauge).strokeLinecap).toBe('round');
+ expect(getComputedStyle(element).height).toBe('64px');
+ });
+
+ it('should assign slotted content to the default slot', async () => {
+ removeFixture(fixture);
+ fixture = await createFixture(html`
+
+ 50%
+
+ `);
+ element = fixture.querySelector(ProgressGauge.metadata.tag);
+ await elementIsStable(element);
+
+ const defaultSlot = element.shadowRoot.querySelector('slot:not([name])') as HTMLSlotElement;
+ const assigned = defaultSlot.assignedElements();
+ expect(assigned).toHaveLength(1);
+ expect(assigned[0]).toBe(fixture.querySelector('span'));
+ });
+
+ it('should set stroke-dasharray to 0 100 when value is 0', async () => {
+ element.value = 0;
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+ expect(gauge.getAttribute('stroke-dasharray')).toBe('0 100');
+ });
+
+ it('should default stroke-dasharray scaling when max is omitted', async () => {
+ element.value = 50;
+ element.max = undefined;
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+ expect(gauge.getAttribute('stroke-dasharray')).toBe('50 100');
+ });
+
+ it('should scale stroke-dasharray with a custom max', async () => {
+ element.value = 5;
+ element.max = 20;
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+ expect(gauge.getAttribute('stroke-dasharray')).toBe('25 100');
+ });
+
+ it('should clamp over-max values', async () => {
+ element.value = 150;
+ element.max = 100;
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+ expect(gauge.getAttribute('stroke-dasharray')).toBe('100 100');
+ expect(element._internals.ariaValueNow).toBe('100');
+ expect(element._internals.ariaValueMax).toBe('100');
+ });
+
+ it.each([
+ { name: 'NaN value', value: Number.NaN, max: 100, dasharray: '0 100', ariaValueNow: '0', ariaValueMax: '100' },
+ { name: 'negative value', value: -1, max: 100, dasharray: '0 100', ariaValueNow: '0', ariaValueMax: '100' },
+ { name: 'NaN max', value: 50, max: Number.NaN, dasharray: '50 100', ariaValueNow: '50', ariaValueMax: '100' },
+ { name: 'zero max', value: 50, max: 0, dasharray: '50 100', ariaValueNow: '50', ariaValueMax: '100' },
+ { name: 'negative max', value: 50, max: -1, dasharray: '50 100', ariaValueNow: '50', ariaValueMax: '100' }
+ ])('should normalize $name', async ({ value, max, dasharray, ariaValueNow, ariaValueMax }) => {
+ element.value = value;
+ element.max = max;
+ await elementIsStable(element);
+
+ const gauge = element.shadowRoot.querySelector('.gauge') as SVGPathElement;
+ expect(gauge.getAttribute('stroke-dasharray')).toBe(dasharray);
+ expect(element._internals.ariaValueNow).toBe(ariaValueNow);
+ expect(element._internals.ariaValueMax).toBe(ariaValueMax);
+ });
+});
diff --git a/projects/core/src/progress-gauge/progress-gauge.test.visual.ts b/projects/core/src/progress-gauge/progress-gauge.test.visual.ts
new file mode 100644
index 000000000..de824257d
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.test.visual.ts
@@ -0,0 +1,69 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect, test, describe } from 'vitest';
+import { visualRunner } from '@internals/vite';
+
+describe('progress-gauge visual', () => {
+ test('progress-gauge should match visual baseline', async () => {
+ const report = await visualRunner.render('progress-gauge', template());
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('progress-gauge should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('progress-gauge.dark', template('dark'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+});
+
+function template(theme: '' | 'dark' = '') {
+ return /* html */ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30Hz
+
+
+ 12Hz
+
+
+ 84%
+
+
+
+
+
+
+
+ 75%
+
+
+ 100%
+
+
+ `;
+}
diff --git a/projects/core/src/progress-gauge/progress-gauge.ts b/projects/core/src/progress-gauge/progress-gauge.ts
new file mode 100644
index 000000000..b40d4b638
--- /dev/null
+++ b/projects/core/src/progress-gauge/progress-gauge.ts
@@ -0,0 +1,113 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import type { PropertyValues } from 'lit';
+import { html, LitElement } from 'lit';
+import { property } from 'lit/decorators/property.js';
+import type { Size, SupportStatus } from '@nvidia-elements/core/internal';
+import { attachInternals, I18nController, useStyles } from '@nvidia-elements/core/internal';
+import styles from './progress-gauge.css?inline';
+
+const GAUGE_GEOMETRY = {
+ default: {
+ path: 'M 27.23 100.77 A 52 52 0 1 1 100.77 100.77',
+ viewBox: '0 0 128 128'
+ },
+ half: {
+ path: 'M 12 64 A 52 52 0 0 1 116 64',
+ viewBox: '0 0 128 64'
+ }
+} as const;
+
+/**
+ * @element nve-progress-gauge
+ * @description Use progress gauge to show system resource usage.
+ * @since 2.0.2
+ * @entrypoint \@nvidia-elements/core/progress-gauge
+ * @slot - Content to display in the gauge center.
+ * @cssprop --gauge-width
+ * @cssprop --accent-color
+ * @cssprop --background
+ * @cssprop --color
+ * @cssprop --width
+ * @cssprop --height
+ * @cssprop --font-size
+ * @aria https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role
+ * @stable false
+ */
+export class ProgressGauge extends LitElement {
+ static styles = useStyles([styles]);
+
+ static readonly metadata = {
+ tag: 'nve-progress-gauge',
+ version: '0.0.0'
+ };
+
+ /** @private */
+ declare _internals: ElementInternals;
+
+ /** The current `value` of the progress gauge. */
+ @property({ type: Number }) value = 0;
+
+ /** The `max` value of the progress gauge that the `value` is proportionally scaled to. */
+ @property({ type: Number }) max? = 100;
+
+ /** Four visual treatments represent the `status` of tasks. */
+ @property({ type: String, reflect: true }) status?: SupportStatus | 'neutral' = 'neutral';
+
+ /** Determines the gauge container shape. Set `half` for a compact semi-circular arc. */
+ @property({ type: String, reflect: true }) container?: 'half';
+
+ /** T-shirt `size` of the progress indicator, used to scale the gauge. */
+ @property({ type: String, reflect: true }) size?: Size;
+
+ #i18nController: I18nController = new I18nController(this);
+
+ /** Enables updating internal string values for internationalization. */
+ @property({ type: Object }) i18n = this.#i18nController.i18n;
+
+ #normalizedValues() {
+ const sourceMax = this.max;
+ const max = sourceMax !== undefined && Number.isFinite(sourceMax) && sourceMax > 0 ? sourceMax : 100;
+ const value = Number.isFinite(this.value) ? Math.min(Math.max(this.value, 0), max) : 0;
+ return { value, max };
+ }
+
+ render() {
+ const geometry = GAUGE_GEOMETRY[this.container ?? 'default'];
+ const { value, max } = this.#normalizedValues();
+ const progress = (value / max) * 100;
+
+ return html`
+
+ `;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ attachInternals(this);
+ this._internals.role = 'progressbar';
+ }
+
+ updated(props: PropertyValues) {
+ super.updated(props);
+ const { value, max } = this.#normalizedValues();
+ this._internals.ariaValueNow = `${value}`;
+ this._internals.ariaValueMax = `${max}`;
+ const i18nRecord = this.i18n as Record;
+ this._internals.ariaLabel =
+ (this.status && i18nRecord[this.status] && i18nRecord[this.status] !== 'neutral'
+ ? i18nRecord[this.status]!
+ : this.i18n.information) ?? null;
+ }
+}
diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js
index 580b90cc9..b76cba85d 100644
--- a/projects/site/src/_11ty/layouts/common.js
+++ b/projects/site/src/_11ty/layouts/common.js
@@ -284,6 +284,7 @@ export const renderDocsNav = data => /* html */ `
Pagination
Progressive Filter Chip
Progress Bar
+ Progress Gauge
Progress Ring
Password
Preferences Input
diff --git a/projects/site/src/docs/elements/progress-gauge.md b/projects/site/src/docs/elements/progress-gauge.md
new file mode 100644
index 000000000..522b41c02
--- /dev/null
+++ b/projects/site/src/docs/elements/progress-gauge.md
@@ -0,0 +1,45 @@
+---
+{
+ title: 'Progress Gauge',
+ layout: 'docs.11ty.js',
+ tag: 'nve-progress-gauge'
+}
+---
+
+## Installation
+
+{% install 'nve-progress-gauge' %}
+
+## Indicating Status
+
+{% api 'nve-progress-gauge', 'property', 'status' %}
+
+{% example '@nvidia-elements/core/progress-gauge/progress-gauge.examples.json' 'Status' %}
+
+## Container
+
+{% api 'nve-progress-gauge', 'property', 'container' %}
+
+{% example '@nvidia-elements/core/progress-gauge/progress-gauge.examples.json' 'Container' %}
+
+## Sizing
+
+{% api 'nve-progress-gauge', 'property', 'size' %}
+
+{% example '@nvidia-elements/core/progress-gauge/progress-gauge.examples.json' 'Sizing' %}
+
+## Value
+
+{% api 'nve-progress-gauge', 'property', 'value' %}
+
+{% example '@nvidia-elements/core/progress-gauge/progress-gauge.examples.json' 'Values' %}
+
+## Max Value
+
+{% api 'nve-progress-gauge', 'property', 'max' %}
+
+{% example '@nvidia-elements/core/progress-gauge/progress-gauge.examples.json' 'Max' %}
+
+## Dynamic
+
+{% example '@nvidia-elements/core/progress-gauge/progress-gauge.examples.json' 'Dynamic' %}