Search code examples
htmlcsstextfield

Customizing Increment Arrows on Input of Type Number Using CSS


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?


Solution

  • 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/[email protected]/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();
    })