splitforms.com
← Back to the journal

How to Remove the Arrows From a Number Input (CSS, Tailwind, React)

Hide the number input spin buttons with ::-webkit-inner-spin-button and appearance: textfield, in Tailwind, and stop scroll from changing the value.

Start free — 500 submissionsSee pricing →No card, no time limit. Pro is $5 per month.
Short answer
Hide the spinner with CSS: set -webkit-appearance: none; margin: 0 on ::-webkit-inner-spin-button and ::-webkit-outer-spin-button, and appearance: textfield on the input for Firefox. In Tailwind, use [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none. If the field is a phone number, ZIP code or code with leading zeros, don't use type="number" at all: use type="text" inputmode="numeric".
Try it on your own form

Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.

Create free account

The copy-paste CSS fix

An input type="number" shows small up and down arrows (the spin buttons) in desktop browsers. Chrome, Edge and Safari draw them with two pseudo-elements. Firefox draws them as part of the input. So you need two rules:

/* Chrome, Edge, Safari, Opera */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Firefox */
input[type=number] {
  -moz-appearance: textfield;
  appearance: textfield;
}

margin: 0 matters: in some versions of Chrome the hidden button still keeps a small margin, which leaves a gap on the right of the field. appearance: textfield tells Firefox to draw the input like a normal text box, without the spinner. The -moz- line is for older Firefox versions that don't support the unprefixed property.

Here is a full form you can paste into any page. It posts to splitforms, so you get the submissions by email and in your dashboard without writing a backend:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />

  <label for="qty">Quantity</label>
  <input id="qty" name="quantity" type="number" min="1" max="20" step="1" required />

  <button type="submit">Send</button>
</form>

<style>
  input[type=number]::-webkit-outer-spin-button,
  input[type=number]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }
  input[type=number] {
    -moz-appearance: textfield;
    appearance: textfield;
  }
</style>

Get a free access key to try it.

Tailwind: arbitrary variants

Tailwind has no built-in class for the spin buttons, but arbitrary properties and variants cover it. Put these three classes on the input:

<input
  type="number"
  name="quantity"
  className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>

[appearance:textfield] is the Firefox rule. The two [&::-webkit-...] variants target the pseudo-elements and set appearance: none on them. In plain HTML use class instead of className. If you hide the arrows on every number field in the project, add the CSS rules from above to your global stylesheet once instead of repeating the classes.

React

In React the CSS is the same. Either import a stylesheet with the rules above, or use the Tailwind classes. The value you read from e.target.value is still a string, so convert it with Number() or e.target.valueAsNumber:

function Quantity() {
  const [qty, setQty] = useState("1");

  return (
    <input
      type="number"
      min={1}
      max={20}
      value={qty}
      onChange={(e) => setQty(e.target.value)}
      className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
    />
  );
}

Hide the arrows on one field only

The global rule removes the spinner from every number input on the page. To keep it on most fields and hide it on one, scope the rules to a class:

.no-spinner::-webkit-outer-spin-button,
.no-spinner::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.no-spinner {
  -moz-appearance: textfield;
  appearance: textfield;
}
<input type="number" name="age" class="no-spinner" />
<input type="number" name="guests" /> <!-- keeps its arrows -->

Stop the scroll wheel changing the value

Hiding the arrows does not stop the other surprise: in some browsers, scrolling the mouse wheel over a focused number input changes its value. A user scrolls down the page and their quantity goes from 3 to -2 without them noticing. The simplest fix is to take focus away when the wheel moves:

document.querySelectorAll('input[type=number]').forEach((input) => {
  input.addEventListener('wheel', (e) => e.currentTarget.blur());
});

The page keeps scrolling and the value stays the same. If you want the field to keep focus, cancel the wheel event only while it is focused. The listener must be non-passive for preventDefault() to work:

input.addEventListener('wheel', (e) => {
  if (document.activeElement === e.currentTarget) e.preventDefault();
}, { passive: false });

In React, onWheel is attached as a passive listener, so preventDefault() there does nothing. Use onWheel={(e) => e.currentTarget.blur()}, or add the non-passive listener yourself in a useEffect with a ref.

When not to use type="number" at all

Often the real fix is a different input type. type="number" is meant for quantities you could add or subtract: an amount, an age, a number of guests. For digits that are really an identifier it causes problems:

  • Leading zeros are lost. A ZIP code like 02134 or a code like 0071 can become 2134 or 71.
  • Other characters are allowed. The browser accepts e, +, - and ., because 1e5 is a valid number.
  • Long numbers break. Card numbers are too long to be stored exactly as a JavaScript number.
  • The spinner makes no sense. Nobody steps a phone number up by one.

For phone numbers, ZIP codes, card numbers, PINs and one-time codes, use a text input with inputmode and pattern:

<input type="text" name="zip" inputmode="numeric" pattern="[0-9]*"
       autocomplete="postal-code" />

inputmode="numeric" opens the number keypad on phones, pattern makes the browser reject anything that isn't digits on submit, and there is no spinner to hide. For a stricter ZIP pattern (5 digits or ZIP+4), test one in the ZIP code regex tester.

min, max and step still work

Removing the arrows only changes how the field looks. Validation is untouched: with min="1" max="20", a value of 25 still blocks the form with the browser's "Value must be less than or equal to 20" message. step still applies too: with step="1", 2.5 is rejected, and with step="0.01" you can accept prices with cents.

<input type="number" name="price" min="0" step="0.01" required />

Always check the value on the server as well. Anyone can send a form request without a browser, so client-side limits are a help for users, not a guarantee.

Accessibility: the keyboard still steps the value

The arrow keys still work after the spin buttons are hidden. With the field focused, Up and Down change the value by one step, and screen readers still announce the field as a spin button with its current value, min and max. That is usually what you want, because keyboard users lose nothing.

The flip side is that mouse users lose the visible hint that the value can be stepped. If stepping matters (a quantity picker in a cart, for example), replace the spinner with clear minus and plus buttons that have accessible labels, such as aria-label="Decrease quantity", and call input.stepDown() and input.stepUp() from them. Those methods respect min, max and step for you.

FAQ

How do I remove the arrows from a number input?

Use CSS. In Chrome, Edge and Safari, set -webkit-appearance: none and margin: 0 on input[type=number]::-webkit-inner-spin-button and ::-webkit-outer-spin-button. In Firefox, set appearance: textfield (and -moz-appearance: textfield for older versions) on the input itself.

How do I hide number input arrows in Tailwind?

Add the arbitrary classes [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none to the input. The first one covers Firefox, the other two hide the WebKit and Blink spin buttons.

Do min, max and step still work when the arrows are hidden?

Yes. Hiding the spinner is only a visual change. The browser still checks min, max and step when the form is submitted, the arrow keys still step the value, and the value is still sent as a number string.

How do I stop the mouse wheel changing a number input?

Some browsers change the value when you scroll over a focused number input. Blur the field on wheel with input.addEventListener("wheel", (e) => e.currentTarget.blur()), or call e.preventDefault() in a non-passive wheel listener while the input has focus.

Should I use type=number for a phone number or ZIP code?

No. type=number drops leading zeros, accepts e and minus signs, and is meant for amounts you count. For phone numbers, ZIP codes, card numbers and one-time codes, use type="text" inputmode="numeric" pattern="[0-9]*". You get the number keypad on phones and no spinner at all.

References: MDN: input type="number" and MDN: appearance.

Related articles

More practical guidance from tutorials.

Browse the journal →
Tutorials

How to Change the Autofill Background Color of an Input (CSS, Tailwind)

Remove Chrome's yellow autofill background with input:-webkit-autofill, inset box-shadow, -w…

7 min readRead →
Tutorials

input type=number maxlength Not Working? 4 Fixes That Work

Why maxlength is ignored on input type=number, plus four fixes: min/max, oninput slice, text…

8 min readRead →
Tutorials

autocomplete="off" Not Working? Why Browsers Ignore It (and Fixes)

Chrome and Edge ignore autocomplete=off on login and address fields. Why browsers override i…

8 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewContact Forms for Every FrameworkStart here →GuideContact Form for Next.jsRead →GuideContact Form for ReactRead →GuideContact Form for VueRead →ReferenceContact form guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 free submissions — no card, no time limit.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe