84

I have an input of type number that is rendered using the following code:

<input class="quantity" id="id_form-0-quantity" min="0" name="form-0-quantity" value="1" type="number">

It looks like this:

enter image description here

I would like to turn it into something like this:

enter image description here

The second view is emulated using two separate buttons.

How could I style the arrows as described?

tao
  • 82,996
  • 16
  • 114
  • 150
MadPhysicist
  • 5,401
  • 11
  • 42
  • 107

6 Answers6

157

tl;dr:

Having been asked in private about the following setup quite a few times, I decided to add a demo for it (Bootstrap 4 + jQuery + Font Awesome input-group setup):

$('.btn-plus, .btn-minus').on('click', function(e) {
  const isNegative = $(e.target).closest('.btn-minus').is('.btn-minus');
  const input = $(e.target).closest('.input-group').find('input');
  if (input.is('input')) {
    input[0][isNegative ? 'stepDown' : 'stepUp']()
  }
})
.inline-group {
  max-width: 9rem;
  padding: .5rem;
}

.inline-group .form-control {
  text-align: right;
}

.form-control[type="number"]::-webkit-inner-spin-button,
.form-control[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>

<div class="input-group inline-group">
  <div class="input-group-prepend">
    <button class="btn btn-outline-secondary btn-minus">
      <i class="fa fa-minus"></i>
    </button>
  </div>
  <input class="form-control quantity" min="0" name="quantity" value="1" type="number">
  <div class="input-group-append">
    <button class="btn btn-outline-secondary btn-plus">
      <i class="fa fa-plus"></i>
    </button>
  </div>
</div>

long (initial) answer:

The native input[type=number] controls are not style-able cross-browser. The easiest and safest way to achieve what you want cross-browser/cross-device is to hide them using:

input[type="number"] {
  -webkit-appearance: textfield;
     -moz-appearance: textfield;
          appearance: textfield;
}
input[type=number]::-webkit-inner-spin-button, 
input[type=number]::-webkit-outer-spin-button { 
  -webkit-appearance: none;
}

...which allows you to use your custom buttons, which could be linked to execute the functions the spinners (arrows) would (.stepUp() and .stepDown()), provided you keep the input's type="number".

For example:

input[type="number"] {
  -webkit-appearance: textfield;
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.number-input {
  border: 2px solid #ddd;
  display: inline-flex;
}

.number-input,
.number-input * {
  box-sizing: border-box;
}

.number-input button {
  outline:none;
  -webkit-appearance: none;
  background-color: transparent;
  border: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.number-input button:before,
.number-input button:after {
  display: inline-block;
  position: absolute;
  content: '';
  width: 1rem;
  height: 2px;
  background-color: #212121;
  transform: translate(-50%, -50%);
}
.number-input button.plus:after {
  transform: translate(-50%, -50%) rotate(90deg);
}

.number-input input[type=number] {
  font-family: sans-serif;
  max-width: 5rem;
  padding: .5rem;
  border: solid #ddd;
  border-width: 0 2px;
  font-size: 2rem;
  height: 3rem;
  font-weight: bold;
  text-align: center;
}
<div class="number-input">
  <button onclick="this.parentNode.querySelector('input[type=number]').stepDown()" ></button>
  <input class="quantity" min="0" name="quantity" value="1" type="number">
  <button onclick="this.parentNode.querySelector('input[type=number]').stepUp()" class="plus"></button>
</div>

Note: In order to change the input's value, one needs to find it. To provide flexibility, in the example above I grouped buttons and the <input> under a common parent and used that parent to find the <input> (choosing not to rely on their proximity or particular order in DOM). The above method will change any input[type=number] sibling to the buttons. If that's not convenient, one could use any other methods to find the input from the buttons:

  • by id: .querySelector('#some-id'):
<button onclick="this.parentNode.querySelector('#some-id').stepUp()"></button>
  • by className: .querySelector('.some-class'):
<button onclick="this.parentNode.querySelector('.some-class').stepUp()"></button>

Also note the above examples only search inside the .parentNode, not in the entire document, which is also possible:
i.e: onclick="document.getElementById('#some-id').stepUp()"

  • by proximity (previousElementSibling | nextElementSibling)
<button onclick="this.previousElementSibling.stepUp()"></button>
  • any other way to determine and find a particular input element in a DOM structure. For example, one could use third party libraries, such as jQuery:
<button onclick="$(this).prev()[0].stepUp()"></button>

An important note when using jQuery is that the stepUp() and stepDown() methods are placed on the DOM element, not on the jQuery wrapper. The DOM element is found inside the 0 property of the jQuery wrapper.


Note on preventDefault(). Clicking a <button> inside a <form> will trigger the form submission. Therefore, if used as above, inside forms, the onclick should also contain preventDefault();. Example:

<button onclick="$(this).prev()[0].stepUp();preventDefault()"></button>

However, if one would use <a> tags instead of <button>s, this is not necessary. Also, the prevention can be set globally for all form buttons with a small JavaScript snippet:

var buttons = document.querySelectorAll('form button:not([type="submit"])');
for (i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function(e) {
    e.preventDefault();
  });
}

... or, using jQuery:

$('form').on('click', 'button:not([type="submit"])', function(e){
  e.preventDefault();
})
tao
  • 82,996
  • 16
  • 114
  • 150
  • Are you saying I could redirect the buttons to affect the native increment controls with the latter being hidden? Could you show how this would be done? – MadPhysicist Jul 30 '17 at 04:40
  • 1
    @AndreiGheorghiu The CSS to hide the spinners does not seem to work for me in your snippet. I am on Windows 8.1 with Firefox(v54.0.1). – Ricky Dam Jul 30 '17 at 05:24
  • 1
    I can't test that now. I'm on Chrome, on Linux. Could you try `input[type=number] { -moz-appearance:textfield; }` and let me know if it works, to add it to answer? It's strange, this worked in all browsers like two months ago or so. I'll have to update it in quite a number of projects... – tao Jul 30 '17 at 05:26
  • 1
    @AndreiGheorghiu Your edit for `-moz-appearance` fixed it. Good stuff. And I tried taking it away and the spinners showed up again. So we definitely need the Mozilla vendor prefix to hide it. – Ricky Dam Jul 30 '17 at 05:38
  • 1
    @RickyDam, from my point of view `appearance:textfield` makes more sense in this case than `appearance: none;`. I checked on both Chrome and FF and it looks good. The note on [MDN's page](https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance) warns about `appearance` being `non-standard` and that even "none" property doesnt work in all browsers the same. Are they pointing to themselves there? Funny. – tao Jul 30 '17 at 05:45
  • 1
    Definitely an odd issue. `-moz-appearance: none;` does not hide the spinners. `-moz-appearance: textfield;` does hide the spinners. CSS is playing mindgames with us ;) – Ricky Dam Jul 30 '17 at 05:51
  • This is pretty awesome! :-) Could you tell me what this is called and where I can read more about it? Is this JS built into HTML? – MadPhysicist Jul 30 '17 at 17:58
  • I am either doing something wrong or there is a problem with this approach. Namely, when I increase or decrease one item, all of the items in the shopping cart react to it. – MadPhysicist Jul 30 '17 at 18:34
  • Most probably you ignored the markup used. I grouped them together. If you prefer to have all in a single container, you have to use ids in the onclick attributes. I'll update to make it clear for anyone else. – tao Jul 31 '17 at 01:25
  • @MadPhysicist today, it would be difficult to think of web without JavaScript. All browsers use it, whether they have a dedicated, stand-alone engine or an embedded, internal one. And it's getting ever more popular server side as well. By far, the most popular use of JavaScript is parsing DOM. See [JavaScript Engine](https://en.wikipedia.org/wiki/JavaScript_engine). To put things into perspective: Chrome runs on probably the most powerful and fastest JS engine, called V8, which also happens to power Node.js. – tao Jul 31 '17 at 02:54
  • Thank you. I understand. Does this mean that you are making JS calls that the browser intrinsically knows about? – MadPhysicist Jul 31 '17 at 03:30
  • 1
    @MadPhysicist, the browser knows about them but this doesn't mean execution will be allowed if the user blocks JavaScript. JavaScript is not exactly safe, because it's client side. For example, one could simply replace `stepUp()` and `stepDown()` methods available on `` elements with different functions which would do something entirely different when they are called. Or do the same thing ***and*** run additional code. – tao Jul 31 '17 at 03:59
  • For some reason, whenever I try the code with the parent, I get the following error: `TypeError: this.parentNode.querySelector(...) is null`. Any idea why this could happen? Also, my code is slightly different in that I have each button nested inside a `div`. Should I be doing something like `this.parentNode.parentNode.querySelector('.quantity').stepUp()`? – MadPhysicist Jul 31 '17 at 17:49
  • 1
    Some should add `; event.preventDefault()` in the onclick attr. – Hugo H Sep 26 '17 at 17:36
  • 1
    @HugoH, technically, that's correct. Clicking a button inside a form without calling `preventDefault()` on the event will trigger the form submission. However, the above code could be used with a link, in which case the prevention is not necessary. Besides, this prevention should be disabled on all form buttons except the one(s) submitting the form and should be done globally for the entire form, not repeated in each `onclick` attribute. – tao Sep 26 '17 at 18:07
  • 1
    @HugoH, I added a note and a small JS snippet for disabling submission on non `[type="submit"]` form buttons. Cheers! – tao Sep 26 '17 at 18:22
  • @HugoH, yes. `To click or not to click...` is perfectly valid. – tao Sep 26 '17 at 18:50
  • Hi, thanks for sharing this code, I was looking for an input control exactly with this shape. – funder7 Mar 16 '19 at 14:14
  • Using this u lose the ability to increment/decrement while holding mouse down. Any workaround for it? – By-Jokese Nov 28 '19 at 18:47
  • 2
    Buttons submit forms by default because their default type is "submit". The correct way to prevent submission is to give them type="button". Then you should not need preventDefault(). – stephband Mar 11 '20 at 12:57
  • Yeah... I was looking for a pure CSS redesign since JS can be disabled and wont work :/ I'll stick with my ugly CSS Input – maiakd Jun 12 '22 at 06:46
12

Number inputs can be stylized this way with the following script I recently found:

Demo.

This script is a super-light-weight (1.49kb uncompressed, 0.71kb compressed, 0.35kb gzipped), usable and reliable quantity input that replaces the horrible, fiddly little input buttons built in to the number input in the browser. It is written using ES6 modules, so will need to be transpiled for older browsers.

The author's repository is here. Hope this helps ;)

Edit: Alternatively, if you'd like to have up/down arrow buttons instead of plus/minus ones, there is another jQuery-based solution.

Number input with up/down arrows

$(document).ready(function () {
  jQuery('<div class="quantity-nav"><button class="quantity-button quantity-up">&#xf106;</button><button class="quantity-button quantity-down">&#xf107</button></div>').insertAfter('.quantity input');
  jQuery('.quantity').each(function () {
    var spinner = jQuery(this),
        input = spinner.find('input[type="number"]'),
        btnUp = spinner.find('.quantity-up'),
        btnDown = spinner.find('.quantity-down'),
        min = input.attr('min'),
        max = input.attr('max');

    btnUp.click(function () {
      var oldValue = parseFloat(input.val());
      if (oldValue >= max) {
        var newVal = oldValue;
      } else {
        var newVal = oldValue + 1;
      }
      spinner.find("input").val(newVal);
      spinner.find("input").trigger("change");
    });

    btnDown.click(function () {
      var oldValue = parseFloat(input.val());
      if (oldValue <= min) {
        var newVal = oldValue;
      } else {
        var newVal = oldValue - 1;
      }
      spinner.find("input").val(newVal);
      spinner.find("input").trigger("change");
    });

  });
});
body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  min-width: 100vw;
  background: #34495E;
  font-size: 1rem;
}

.quantity {
  position: relative;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

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

.quantity input {
  width: 45px;
  height: 42px;
  line-height: 1.65;
  float: left;
  display: block;
  padding: 0;
  margin: 0;
  padding-left: 20px;
  border: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
  font-size: 1rem;
  border-radius: 4px;
}

.quantity input:focus {
  outline: 0;
}

.quantity-nav {
  float: left;
  position: relative;
  height: 42px;
}

.quantity-button {
  position: relative;
  cursor: pointer;
  border: none;
  border-left: 1px solid rgba(0, 0, 0, 0.08);
  width: 21px;
  text-align: center;
  color: #333;
  font-size: 13px;
  font-family: "FontAwesome" !important;
  line-height: 1.5;
  padding: 0;
  background: #FAFAFA;
  -webkit-transform: translateX(-100%);
  transform: translateX(-100%);
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -o-user-select: none;
  user-select: none;
}

.quantity-button:active {
  background: #EAEAEA;
}

.quantity-button.quantity-up {
  position: absolute;
  height: 50%;
  top: 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  font-family: "FontAwesome";
  border-radius: 0 4px 0 0;
  line-height: 1.6
}

.quantity-button.quantity-down {
  position: absolute;
  bottom: 0;
  height: 50%;
  font-family: "FontAwesome";
  border-radius: 0 0 4px 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="quantity">
  <input type="number" min="1" max="9" step="1" value="1">
</div>
s.kuznetsov
  • 14,870
  • 3
  • 10
  • 25
Nekto
  • 327
  • 1
  • 4
  • 16
12

Here is another version, based on the answer of @tao, using font-awesome:

input[type="number"] {
  -webkit-appearance: textfield;
  -moz-appearance: textfield;
  appearance: textfield;
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.number-input {
  border: 2px solid #ddd;
  display: inline-flex;
}

.number-input,
.number-input * {
  box-sizing: border-box;
}

.number-input button {
  outline:none;
  -webkit-appearance: none;
  background-color: transparent;
  border: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.number-input button:after {
  display: inline-block;
  position: absolute;
  font-family: "Font Awesome 5 Free"; 
  font-weight: 900;
  content: '\f077';
  transform: translate(-50%, -50%) rotate(180deg);
}
.number-input button.plus:after {
  transform: translate(-50%, -50%) rotate(0deg);
}

.number-input input[type=number] {
  font-family: sans-serif;
  max-width: 5rem;
  padding: .5rem;
  border: solid #ddd;
  border-width: 0 2px;
  font-size: 2rem;
  height: 3rem;
  font-weight: bold;
  text-align: center;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w==" crossorigin="anonymous" />

<div class="number-input">
  <button onclick="this.parentNode.querySelector('input[type=number]').stepDown()" class="minus"></button>
  <input class="quantity" min="0" name="quantity" value="1" type="number">
  <button onclick="this.parentNode.querySelector('input[type=number]').stepUp()" class="plus"></button>
</div>
Black
  • 18,150
  • 39
  • 158
  • 271
3

You can easily convert first design with second one like this:

HTML

<div class="quantity">
    <button class="btn minus1">-</button>
    <input class="quantity" id="id_form-0-quantity" min="0" name="form-0-quantity" value="1" type="number">
    <button class="btn add1">+</button>

CSS

.quantity{
   display:flex;
   width:160px;
}

/* it will support chrome and firefox */
.quantity input[type=number]::-webkit-inner-spin-button,
.quantity input[type=number]::-webkit-outer-spin-button{
   -webkit-appearance:none;
}

.quantity input,.quantity button{
   width:50px;
   padding:.5em;
   font-size:1.2rem;
   text-align:center;
   font-weight:900;
   background:white;
   border:1px solid #aaa;
}

.quantity input{
   border-left:none;
   border-right:none;
}
Aslam khan
  • 315
  • 1
  • 7
  • 2
    You don't get any functionality with this, though – Maharkus May 15 '20 at 07:28
  • 2
    this is the best solution. you get the functionality by adding an onClick handler to your button elements. dont expect Mr. Khan to build the entire project for you, he's just answering the question. – lopezdp Nov 29 '20 at 12:56
  • 1
    Thanks, it's good for me. This solution is lightweight for basic styling. I've copied the code to a codepen for easy viewing: https://codepen.io/sillycube/pen/ZEJJQQx – Benny Chan Oct 29 '21 at 03:49
3

I found a nice solution. Just rotate the arrow keys and set the opacity to 0. (they are now in the right place, invisible but clickable) Then set an :after and :before element over these invisible buttons. These elements can then be styled as desired.

input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  transform: rotate(90deg);
  height: 80px;
  opacity: 0;
}

.quantity-wrapper {
  position: relative;
}

.quantity-wrapper:after {
  content: "+";
  position: absolute;
  right: 5px;
  height: 100%;
  top: 8px;
  pointer-events: none;
}

.quantity-wrapper:before {
  content: "-";
  position: absolute;
  left: 5px;
  height: 100%;
  top: 8px;
}
<div class="quantity-wrapper">
  <input class="quantity" id="id_form-0-quantity" min="0" name="form-0-quantity" value="1" type="number">
</div>
MarLMazo
  • 424
  • 1
  • 5
  • 18
  • 1
    It would be helpful to others if you post a working demo. – MadPhysicist May 19 '20 at 13:37
  • Given the code sample, it might help also to know that MDN considers [`::-webkit-inner-spin-button`](https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-inner-spin-button) and [`::-webkit-outer-spin-button`](https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-outer-spin-button) as non-standard and "not on a standards track". Having said that, the former is apparently supported on all but Firefox and IE, whereas the latter is widely unsupported (according to figures in links). – Nelson Frew Jan 06 '22 at 09:24
  • 1
    This looks exactly like a regular number field? – Andrew West Apr 26 '22 at 12:03
1

This code can help you .may be found [here also](Detecting number input spinner click

$.fn.spinInput = function (options) {
    var settings = $.extend({
        maximum: 1000,
        minimum: 0,
        value: 1,
        onChange: null
    }, options);

    return this.each(function (index, item) {
        var min = $(item).find('>*:first-child').first();
        var max = $(item).find('>*:last-child').first();
        var v_span = $(item).find('>*:nth-child(2)').find('span');
        var v_input = $(item).find('>*:nth-child(2)').find('input');
        var value = settings.value;
        $(v_input).val(value);
        $(v_span).text(value);
        async function increment() {
            value = Number.parseInt($(v_input).val());
            if ((value - 1) > settings.maximum) return;
            value++;
            $(v_input).val(value);
            $(v_span).text(value);
            if (settings.onChange) settings.onChange(value);
        }
        async function desincrement() {
            value = Number.parseInt($(v_input).val());
            if ((value - 1) < settings.minimum) return;
            value--
            $(v_input).val(value);
            $(v_span).text(value);
            if (settings.onChange) settings.onChange(value);
        }
        var pressTimer;

        function actionHandler(btn, fct, time = 100, ...args) {
            function longHandler() {
                pressTimer = window.setTimeout(function () {
                    fct(...args);
                    clearTimeout(pressTimer);
                    longHandler()
                }, time);
            }
            $(btn).mouseup(function () {
                clearTimeout(pressTimer);
            }).mousedown(function () {
                longHandler();
            });

            $(btn).click(function () {
                fct(...args);
            });
        }

        actionHandler(min, desincrement, 100);
        actionHandler(max, increment, 100)
    })
}




$('body').ready(function () {
    $('.spin-input').spinInput({ value: 1, minimum: 1 });
});
:root {
    --primary-dark-color: #F3283C;
    --primary-light-color: #FF6978;
    --success-dark-color: #32A071;
    --sucess-light-color: #06E775;
    --alert-light-color: #a42a23;
    --alert-dark-color: #7a1f1a;
    --secondary-dark-color: #666666;
    --secondary-light-color: #A6A6A6;
    --gold-dark-color: #FFA500;
    --gold-light-color: #FFBD00;
    --default-dark-color: #1E2C31;
    --default-light-color: #E5E5E5;
}

.fx-row {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}

.fx-colum {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
}

.fx-colum.nowrap,
.fx-row.nowrap {
    flex-wrap: nowrap;
}

.fx-row.fx-fill>*,
.fx-colum.fx-fill>* {
    flex-grow: 1;
}

.spin-input {
    border: 1px solid var(--secondary-light-color);
}

.spin-input>div:first-child {
    cursor: pointer;
    border-right: 1px solid var(--secondary-light-color);
}

.spin-input>div:first-child:active {
    transform: translate3d(1px, 0px, 1px)
}

.spin-input>div:last-child {
    flex: none;
    border-left: 1px solid var(--secondary-light-color);
    cursor: pointer;
}

.spin-input>div:last-child:active {
    transform: translate3d(1px, 0px, 1px)
}

.icon {
    font-weight: bold;
    text-align: center;
    vertical-align: middle;
    padding: 12px;
    font-size: 28px;
}

.icon.primary,
.icon.primary .ci {
    color: var(--primary-dark-color);
}

.icon.reactive:hover .ci {
    color: var(--primary-light-color);
}

.hidden {
    display: none;
}
<script src="https://releases.jquery.com/git/jquery-3.x-git.min.js"></script>
<div class="spin-input nowrap fx-row fx-fill" >
                        <div class="icon reactive">
                            <span class="ci ci-minus">-</span>
                        </div>
                        <div class="icon">
                            <span>0</span>
                            <input type="text" class="hidden" value="0">
                        </div>
                        <div class="icon reactive">
                            <span class="ci ci-plus">+</span>
                        </div>
                    </div>

259)