diff --git a/projects/components/src/number-input/number-input-appearance.ts b/projects/components/src/number-input/number-input-appearance.ts
new file mode 100644
index 000000000..f6781ee6b
--- /dev/null
+++ b/projects/components/src/number-input/number-input-appearance.ts
@@ -0,0 +1,4 @@
+export const enum NumberInputAppearance {
+ Underline = 'underline',
+ Border = 'border'
+}
diff --git a/projects/components/src/number-input/number-input.component.scss b/projects/components/src/number-input/number-input.component.scss
new file mode 100644
index 000000000..0722b7522
--- /dev/null
+++ b/projects/components/src/number-input/number-input.component.scss
@@ -0,0 +1,31 @@
+@import 'font';
+@import 'color-palette';
+
+.number-input {
+ @include body-2-regular($gray-9);
+ width: inherit;
+ height: inherit;
+ background: white;
+ text-align: center;
+}
+
+.border {
+ border: 1px solid $gray-2;
+ border-radius: 6px;
+}
+
+.underline {
+ border: none;
+ border-bottom: 1px solid $gray-2;
+}
+
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+/* Firefox */
+input[type='number'] {
+ -moz-appearance: textfield;
+}
diff --git a/projects/components/src/number-input/number-input.component.test.ts b/projects/components/src/number-input/number-input.component.test.ts
new file mode 100644
index 000000000..e053493cb
--- /dev/null
+++ b/projects/components/src/number-input/number-input.component.test.ts
@@ -0,0 +1,71 @@
+import { fakeAsync } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { createHostFactory } from '@ngneat/spectator/jest';
+import { NumberInputAppearance } from './number-input-appearance';
+import { NumberInputComponent } from './number-input.component';
+
+describe('Number Input Component', () => {
+ const hostFactory = createHostFactory({
+ component: NumberInputComponent,
+ imports: [FormsModule],
+ shallow: true
+ });
+
+ test('should apply disabled attribute when disabled', fakeAsync(() => {
+ const spectator = hostFactory(`
+
+ `);
+
+ spectator.tick();
+ expect(spectator.query('input')!.getAttributeNames().includes('disabled')).toBe(true);
+ }));
+
+ test('should apply border style correctly', fakeAsync(() => {
+ const spectator = hostFactory(
+ `
+
+ `,
+ {
+ hostProps: {
+ appearance: NumberInputAppearance.Border
+ }
+ }
+ );
+
+ spectator.tick();
+ expect(spectator.query('input')?.classList).toContain('border');
+ }));
+
+ test('should apply underline style correctly', fakeAsync(() => {
+ const spectator = hostFactory(
+ `
+
+ `,
+ {
+ hostProps: {
+ appearance: NumberInputAppearance.Underline
+ }
+ }
+ );
+
+ spectator.tick();
+ expect(spectator.query('input')?.classList).toContain('underline');
+ }));
+
+ test('should emit correct values on value change', fakeAsync(() => {
+ const spectator = hostFactory(`
+
+ `);
+
+ spectator.tick();
+ spyOn(spectator.component.valueChange, 'emit');
+ spectator.triggerEventHandler('input', 'ngModelChange', 7);
+ expect(spectator.component.valueChange.emit).toHaveBeenCalledWith(7);
+
+ spectator.triggerEventHandler('input', 'ngModelChange', 15);
+ expect(spectator.component.valueChange.emit).toHaveBeenCalledWith(10);
+
+ spectator.triggerEventHandler('input', 'ngModelChange', -1);
+ expect(spectator.component.valueChange.emit).toHaveBeenCalledWith(1);
+ }));
+});
diff --git a/projects/components/src/number-input/number-input.component.ts b/projects/components/src/number-input/number-input.component.ts
new file mode 100644
index 000000000..ee5b66559
--- /dev/null
+++ b/projects/components/src/number-input/number-input.component.ts
@@ -0,0 +1,58 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+import { NumberCoercer } from '@hypertrace/common';
+import { NumberInputAppearance } from './number-input-appearance';
+
+@Component({
+ selector: 'ht-number-input',
+ styleUrls: ['./number-input.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ `
+})
+export class NumberInputComponent {
+ @Input()
+ public value?: number;
+
+ @Input()
+ public appearance: NumberInputAppearance = NumberInputAppearance.Border;
+
+ @Input()
+ public disabled: boolean = false;
+
+ @Input()
+ public minValue?: number;
+
+ @Input()
+ public maxValue?: number;
+
+ @Output()
+ public readonly valueChange: EventEmitter = new EventEmitter();
+
+ private readonly numberCoercer: NumberCoercer = new NumberCoercer();
+
+ private enforceMinMaxAndEmit(): void {
+ if (this.value !== undefined && this.maxValue !== undefined && this.value > this.maxValue) {
+ this.value = this.maxValue;
+ }
+
+ if (this.value !== undefined && this.minValue !== undefined && this.value < this.minValue) {
+ this.value = this.minValue;
+ }
+
+ this.valueChange.emit(this.numberCoercer.coerce(this.value));
+ }
+
+ public onValueChange(value?: number): void {
+ this.value = value;
+
+ this.enforceMinMaxAndEmit();
+ }
+}
diff --git a/projects/components/src/number-input/number-input.module.ts b/projects/components/src/number-input/number-input.module.ts
new file mode 100644
index 000000000..74fb4d1ab
--- /dev/null
+++ b/projects/components/src/number-input/number-input.module.ts
@@ -0,0 +1,11 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { NumberInputComponent } from './number-input.component';
+
+@NgModule({
+ imports: [CommonModule, FormsModule],
+ declarations: [NumberInputComponent],
+ exports: [NumberInputComponent]
+})
+export class NumberInputModule {}
diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts
index 60b815fe6..2ac3df788 100644
--- a/projects/components/src/public-api.ts
+++ b/projects/components/src/public-api.ts
@@ -124,6 +124,11 @@ export { InputAppearance } from './input/input-appearance';
export * from './input/input.component';
export * from './input/input.module';
+// Number Input
+export { NumberInputAppearance } from './number-input/number-input-appearance';
+export * from './number-input/number-input.component';
+export * from './number-input/number-input.module';
+
// Json Tree
export { JsonViewerComponent } from './viewer/json-viewer/json-viewer.component';
export { JsonViewerModule } from './viewer/json-viewer/json-viewer.module';