diff --git a/src/editor/components/seDropdown.js b/src/editor/components/seDropdown.js
index 91accd1f..50061ff4 100644
--- a/src/editor/components/seDropdown.js
+++ b/src/editor/components/seDropdown.js
@@ -1,10 +1,9 @@
/* eslint-disable node/no-unpublished-import */
import ListComboBox from 'elix/define/ListComboBox.js';
-import NumberSpinBox from 'elix/define/NumberSpinBox.js';
-// import Input from 'elix/src/base/Input.js';
import {defaultState} from 'elix/src/base/internal.js';
import {templateFrom, fragmentFrom} from 'elix/src/core/htmlLiterals.js';
import {internal} from 'elix';
+import NumberSpinBox from '../dialogs/se-elix/define/NumberSpinBox.js';
/**
* @class Dropdown
diff --git a/src/editor/components/seSpinInput.js b/src/editor/components/seSpinInput.js
index 19cbc6bb..c4722270 100644
--- a/src/editor/components/seSpinInput.js
+++ b/src/editor/components/seSpinInput.js
@@ -1,5 +1,5 @@
/* eslint-disable node/no-unpublished-import */
-import 'elix/define/NumberSpinBox.js';
+import '../dialogs/se-elix/define/NumberSpinBox.js';
const template = document.createElement('template');
template.innerHTML = `
diff --git a/src/editor/components/seZoom.js b/src/editor/components/seZoom.js
index 0f95a2ab..0a403030 100644
--- a/src/editor/components/seZoom.js
+++ b/src/editor/components/seZoom.js
@@ -1,9 +1,8 @@
/* eslint-disable node/no-unpublished-import */
import ListComboBox from 'elix/define/ListComboBox.js';
-import NumberSpinBox from 'elix/define/NumberSpinBox.js';
-// import Input from 'elix/src/base/Input.js';
import * as internal from 'elix/src/base/internal.js';
import {templateFrom, fragmentFrom} from 'elix/src/core/htmlLiterals.js';
+import NumberSpinBox from '../dialogs/se-elix/define/NumberSpinBox.js';
/**
* @class Dropdown
diff --git a/src/editor/dialogs/exportDialog.js b/src/editor/dialogs/exportDialog.js
index efd54619..ff5a52a4 100644
--- a/src/editor/dialogs/exportDialog.js
+++ b/src/editor/dialogs/exportDialog.js
@@ -1,7 +1,6 @@
-/* eslint-disable max-len */
/* eslint-disable node/no-unpublished-import */
import 'elix/define/Dialog.js';
-import 'elix/define/NumberSpinBox.js';
+import './se-elix/define/NumberSpinBox.js';
const template = document.createElement('template');
template.innerHTML = `
diff --git a/src/editor/dialogs/se-elix/define/NumberSpinBox.js b/src/editor/dialogs/se-elix/define/NumberSpinBox.js
new file mode 100644
index 00000000..034eafbc
--- /dev/null
+++ b/src/editor/dialogs/se-elix/define/NumberSpinBox.js
@@ -0,0 +1,7 @@
+import PlainNumberSpinBox from '../src/plain/PlainNumberSpinBox.js';
+/**
+ * @class ElixNumberSpinBox
+ */
+export default class ElixNumberSpinBox extends PlainNumberSpinBox {}
+
+customElements.define('elix-number-spin-box', ElixNumberSpinBox);
diff --git a/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js b/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js
new file mode 100644
index 00000000..7e2b9512
--- /dev/null
+++ b/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js
@@ -0,0 +1,250 @@
+/* eslint-disable node/no-unpublished-import */
+import {
+ defaultState,
+ setState,
+ state,
+ stateEffects
+} from 'elix/src/base/internal.js';
+import {
+ SpinBox
+} from 'elix/src/base/SpinBox.js';
+
+/**
+ * @class NumberSpinBox
+ */
+class NumberSpinBox extends SpinBox {
+ /**
+ * @function attributeChangedCallback
+ * @param {string} name
+ * @param {string} oldValue
+ * @param {string} newValue
+ * @returns {void}
+ */
+ attributeChangedCallback (name, oldValue, newValue) {
+ if (name === 'max') {
+ this.max = parseFloat(newValue);
+ } else if (name === 'min') {
+ this.min = parseFloat(newValue);
+ } else if (name === 'step') {
+ this.step = parseFloat(newValue);
+ } else {
+ super.attributeChangedCallback(name, oldValue, newValue);
+ }
+ }
+ /**
+ * @function observedAttributes
+ * @returns {any} observed
+ */
+ get [defaultState] () {
+ return Object.assign(super[defaultState], {
+ max: null,
+ min: null,
+ step: 1
+ });
+ }
+
+ /**
+ * Format the numeric value as a string.
+ *
+ * This is used after incrementing/decrementing the value to reformat the
+ * value as a string.
+ *
+ * @param {number} value
+ * @param {number} precision
+ */
+ formatValue (value, precision) {
+ return Number(value).toFixed(precision);
+ }
+
+ /**
+ * The maximum allowable value of the `value` property.
+ *
+ * @type {number|null}
+ * @default 1
+ */
+ get max () {
+ return this[state].max;
+ }
+ /**
+ * The maximum allowable value of the `value` property.
+ *
+ * @type {number|null}
+ * @default 1
+ */
+ set max (max) {
+ this[setState]({
+ max
+ });
+ }
+
+ /**
+ * The minimum allowable value of the `value` property.
+ *
+ * @type {number|null}
+ * @default 1
+ */
+ get min () {
+ return this[state].min;
+ }
+ /**
+ * @function set
+ * @returns {void}
+ */
+ set min (min) {
+ this[setState]({
+ min
+ });
+ }
+
+ /**
+ * Parse the given string as a number.
+ *
+ * This is used to parse the current value before incrementing/decrementing
+ * it.
+ *
+ * @param {string} value
+ * @param {number} precision
+ */
+ parseValue(value, precision) {
+ const parsed = precision === 0 ? parseInt(value) : parseFloat(value);
+ return isNaN(parsed) ? 0 : parsed;
+ }
+
+ [stateEffects] (state, changed) {
+ const effects = super[stateEffects];
+ // If step changed, calculate its precision (number of digits after
+ // the decimal).
+ if (changed.step) {
+ const {
+ step
+ } = state;
+ const decimalRegex = /\.(\d)+$/;
+ const match = decimalRegex.exec(String(step));
+ const precision = match && match[1] ? match[1].length : 0;
+ Object.assign(effects, {
+ precision
+ });
+ }
+
+ if (changed.max || changed.min || changed.value) {
+ // The value is valid if it falls between the min and max.
+ // TODO: We need a way to let other classes/mixins on the prototype chain
+ // contribute to validity -- if someone else thinks the value is invalid,
+ // we should respect that, even if the value falls within the min/max
+ // bounds.
+ const {
+ max,
+ min,
+ precision,
+ value
+ } = state;
+ const parsed = parseInt(value, precision);
+ if (value !== '' && isNaN(parsed)) {
+ Object.assign(effects, {
+ valid: false,
+ validationMessage: 'Value must be a number'
+ });
+ } else if (!(max === null || parsed <= max)) {
+ Object.assign(effects, {
+ valid: false,
+ validationMessage: `Value must be less than or equal to ${max}.`
+ });
+ } else if (!(min === null || parsed >= min)) {
+ Object.assign(effects, {
+ valid: false,
+ validationMessage: `Value must be greater than or equal to ${min}.`
+ });
+ } else {
+ Object.assign(effects, {
+ valid: true,
+ validationMessage: ''
+ });
+ }
+ // We can only go up if we're below max.
+ Object.assign(effects, {
+ canGoUp: isNaN(parsed) || state.max === null || parsed <= state.max
+ });
+
+ // We can only go down if we're above min.
+ Object.assign(effects, {
+ canGoDown: isNaN(parsed) || state.min === null || parsed >= state.min
+ });
+ }
+
+ return effects;
+ }
+
+ /**
+ * The amount by which the `value` will be incremented or decremented.
+ *
+ * The precision of the step (the number of digits after any decimal)
+ * determines how the spin box will format the number. The default `step`
+ * value of 1 has no decimals, so the `value` will be formatted as an integer.
+ * A `step` of 0.1 will format the `value` as a number with one decimal place.
+ *
+ * @type {number}
+ * @default 1
+ */
+ get step () {
+ return this[state].step;
+ }
+ set step (step) {
+ if (!isNaN(step)) {
+ this[setState]({
+ step
+ });
+ }
+ }
+
+ /**
+ * Decrements the `value` by the amount of the `step`.
+ *
+ * If the result is still greater than the `max` value, this will force
+ * `value` to `max`.
+ */
+ stepDown () {
+ super.stepDown();
+ const {
+ max,
+ precision,
+ value
+ } = this[state];
+ let result = this.parseValue(value, precision) - this.step;
+ if (max !== null) {
+ result = Math.min(result, max);
+ }
+ const {
+ min
+ } = this[state];
+ if (min === null || result >= min) {
+ this.value = this.formatValue(result, precision);
+ }
+ }
+
+ /**
+ * Increments the `value` by the amount of the `step`.
+ *
+ * If the result is still smaller than the `min` value, this will force
+ * `value` to `min`.
+ */
+ stepUp () {
+ super.stepUp();
+ const {
+ min,
+ precision,
+ value
+ } = this[state];
+ let result = this.parseValue(value, precision) + this.step;
+ if (min !== null) {
+ result = Math.max(result, min);
+ }
+ const {
+ max
+ } = this[state];
+ if (max === null || result <= max) {
+ this.value = this.formatValue(result, precision);
+ }
+ }
+}
+
+export default NumberSpinBox;
diff --git a/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js b/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js
new file mode 100644
index 00000000..a86af29e
--- /dev/null
+++ b/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js
@@ -0,0 +1,10 @@
+/* eslint-disable node/no-unpublished-import */
+import PlainSpinBoxMixin from 'elix/src/plain/PlainSpinBoxMixin.js';
+import NumberSpinBox from '../base/NumberSpinBox.js';
+
+/**
+ * @class PlainNumberSpinBox
+ */
+class PlainNumberSpinBox extends PlainSpinBoxMixin(NumberSpinBox) {}
+
+export default PlainNumberSpinBox;
diff --git a/src/editor/index.html b/src/editor/index.html
index 8d14f2d9..a12f52ac 100644
--- a/src/editor/index.html
+++ b/src/editor/index.html
@@ -349,7 +349,7 @@
-
+