+
+## Sample Usage
+
+This is a number spinner component that is designed to be highly extendable.
+
+Example:
+
+```js
+// Import the component as needed:
+import '@travelopia/web-components/dist/number-spinner';
+
+// TypeScript usage:
+import { TPNumberSpinner, TPNumberSpinnerInput, TPNumberSpinnerIncrement, TPNumberSpinnerDecrement } from '@travelopia/web-components';
+
+```
+
+```html
+
+
+
+
+
+
+
+```
+
+## Attributes
+
+| Attribute | Required | Values | Notes |
+|-----------|----------|-----------------------|----------------------------------------|
+| min | No | | The minimum value of the spinner |
+| max | No | | The maxium value of the spinner |
+| step | No | | The step of the spinner. Defaults to 1 |
diff --git a/src/number-spinner/index.html b/src/number-spinner/index.html
new file mode 100644
index 0000000..fd8c011
--- /dev/null
+++ b/src/number-spinner/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ Web Component: Number Spinner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/number-spinner/index.ts b/src/number-spinner/index.ts
new file mode 100644
index 0000000..de0506e
--- /dev/null
+++ b/src/number-spinner/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Styles.
+ */
+import './style.scss';
+
+/**
+ * Components.
+ */
+import { TPNumberSpinnerInput } from './tp-number-spinner-input';
+import { TPNumberSpinnerIncrement } from './tp-number-spinner-increment';
+import { TPNumberSpinnerDecrement } from './tp-number-spinner-decrement';
+import { TPNumberSpinner } from './tp-number-spinner';
+
+/**
+ * Register Components.
+ */
+customElements.define( 'tp-number-spinner-input', TPNumberSpinnerInput );
+customElements.define( 'tp-number-spinner-increment', TPNumberSpinnerIncrement );
+customElements.define( 'tp-number-spinner-decrement', TPNumberSpinnerDecrement );
+customElements.define( 'tp-number-spinner', TPNumberSpinner );
diff --git a/src/number-spinner/style.scss b/src/number-spinner/style.scss
new file mode 100644
index 0000000..6d02b9a
--- /dev/null
+++ b/src/number-spinner/style.scss
@@ -0,0 +1,3 @@
+tp-number-spinner {
+ display: contents;
+}
diff --git a/src/number-spinner/tp-number-spinner-decrement.ts b/src/number-spinner/tp-number-spinner-decrement.ts
new file mode 100644
index 0000000..a5d8cb4
--- /dev/null
+++ b/src/number-spinner/tp-number-spinner-decrement.ts
@@ -0,0 +1,29 @@
+/**
+ * Internal dependencies.
+ */
+import { TPNumberSpinner } from './tp-number-spinner';
+
+/**
+ * TP Number Spinner Decrement Element.
+ */
+export class TPNumberSpinnerDecrement extends HTMLElement {
+ /**
+ * Constructor.
+ */
+ constructor() {
+ // Initialize parent.
+ super();
+
+ // Attach click event for decrement.
+ this.querySelector( 'button' )?.addEventListener( 'click', this.decrement.bind( this ) );
+ }
+
+ /**
+ * Decrement the value.
+ */
+ decrement(): void {
+ // Run function on parent.
+ const numberSpinner: TPNumberSpinner | null = this.closest( 'tp-number-spinner' );
+ numberSpinner?.decrement();
+ }
+}
diff --git a/src/number-spinner/tp-number-spinner-increment.ts b/src/number-spinner/tp-number-spinner-increment.ts
new file mode 100644
index 0000000..ace0abc
--- /dev/null
+++ b/src/number-spinner/tp-number-spinner-increment.ts
@@ -0,0 +1,29 @@
+/**
+ * Internal dependencies.
+ */
+import { TPNumberSpinner } from './tp-number-spinner';
+
+/**
+ * TP Number Spinner Increment Element.
+ */
+export class TPNumberSpinnerIncrement extends HTMLElement {
+ /**
+ * Constructor.
+ */
+ constructor() {
+ // Initialize parent.
+ super();
+
+ // Attach click event for increment.
+ this.querySelector( 'button' )?.addEventListener( 'click', this.increment.bind( this ) );
+ }
+
+ /**
+ * Increment the value.
+ */
+ increment(): void {
+ // Run function on parent.
+ const numberSpinner: TPNumberSpinner | null = this.closest( 'tp-number-spinner' );
+ numberSpinner?.increment();
+ }
+}
diff --git a/src/number-spinner/tp-number-spinner-input.ts b/src/number-spinner/tp-number-spinner-input.ts
new file mode 100644
index 0000000..b5b16aa
--- /dev/null
+++ b/src/number-spinner/tp-number-spinner-input.ts
@@ -0,0 +1,5 @@
+/**
+ * TP Number Spinner Input.
+ */
+export class TPNumberSpinnerInput extends HTMLElement {
+}
diff --git a/src/number-spinner/tp-number-spinner.ts b/src/number-spinner/tp-number-spinner.ts
new file mode 100644
index 0000000..931489c
--- /dev/null
+++ b/src/number-spinner/tp-number-spinner.ts
@@ -0,0 +1,140 @@
+/**
+ * TP Number Spinner Element.
+ */
+export class TPNumberSpinner extends HTMLElement {
+ /**
+ * Get minimum value.
+ *
+ * @return {number|null} The minimum value.
+ */
+ get min(): number | null {
+ // Get minimum attribute.
+ const min: string | null = this.getAttribute( 'min' );
+
+ // Check if we have an attribute.
+ if ( min ) {
+ // Yep, return its value.
+ return parseInt( min );
+ }
+
+ // Nope, return null.
+ return null;
+ }
+
+ /**
+ * Set minimum value.
+ *
+ * @param {number} min Minimum value.
+ */
+ set min( min: number ) {
+ // Set attribute.
+ this.setAttribute( 'min', min.toString() );
+ }
+
+ /**
+ * Get maximum value.
+ *
+ * @return {number|null} The maximum value.
+ */
+ get max(): number | null {
+ // Get maximum attribute.
+ const max: string | null = this.getAttribute( 'max' );
+
+ // Check if we have an attribute.
+ if ( max ) {
+ // Yep, return its value.
+ return parseInt( max );
+ }
+
+ // Nope, return null.
+ return null;
+ }
+
+ /**
+ * Set maximum value.
+ *
+ * @param {number} max Maximum value.
+ */
+ set max( max: number ) {
+ // Set attribute.
+ this.setAttribute( 'max', max.toString() );
+ }
+
+ /**
+ * Get current step.
+ *
+ * @return {number} Current step.
+ */
+ get step(): number {
+ // Get step from attribute.
+ return parseInt( this.getAttribute( 'step' ) ?? '1' );
+ }
+
+ /**
+ * Set current step.
+ *
+ * @param {number} step Current step.
+ */
+ set step( step: number ) {
+ // Set attribute.
+ this.setAttribute( 'step', step.toString() );
+ }
+
+ /**
+ * Get value.
+ *
+ * @return {number} The value.
+ */
+ get value(): number {
+ // Get value from input.
+ return parseInt( this.querySelector( 'tp-number-spinner-input input' )?.getAttribute( 'value' ) ?? '0' );
+ }
+
+ /**
+ * Set current value.
+ *
+ * @param {number} value Current value.
+ */
+ set value( value: number ) {
+ // Set input's value.
+ this.querySelector( 'tp-number-spinner-input input' )?.setAttribute( 'value', value.toString() );
+ }
+
+ /**
+ * Increment.
+ */
+ increment(): void {
+ // Calculate new value.
+ const currentValue: number = this.value;
+ const max: number | null = this.max;
+ const newValue: number = currentValue + this.step;
+
+ // Check if new value is greater than the maximum.
+ if ( max && newValue > max ) {
+ // Yes, that's not allowed.
+ return;
+ }
+
+ // No, set its value.
+ this.value = newValue;
+ }
+
+ /**
+ * Decrement.
+ */
+ decrement(): void {
+ // Calculate new value.
+ const currentValue: number = this.value;
+ const min: number | null = this.min;
+ const newValue: number = currentValue - this.step;
+
+ // Check if new value is less than the minumum.
+ if ( min && newValue < min ) {
+ // Yes, that's not allowed.
+ return;
+ }
+
+ // No, set its value.
+ this.value = newValue;
+ }
+}
diff --git a/webpack.config.js b/webpack.config.js
index 67df031..a347deb 100755
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -84,6 +84,7 @@ module.exports = ( env ) => {
'multi-select': './src/multi-select/index.ts',
lightbox: './src/lightbox/index.ts',
'toggle-attribute': './src/toggle-attribute/index.ts',
+ 'number-spinner': './src/number-spinner/index.ts',
},
module: {
rules: [