Add a text suffix to <input type="number">

You can use a wrapper <div> for each input element and position the unit as a pseudo element ::after with the content of your corresponding units.

This approach works well for the absolute positioned pseudo elements will not effect the existing layouts. Nevertheless, the downside of this approach is, that you have to make sure, that the user input is not as long as the text field, otherwise the unit will be unpleasantly shown above. For a fixed user input length, it should work fine.

/* prepare wrapper element */
div {
  display: inline-block;
  position: relative;
}

/* position the unit to the right of the wrapper */
div::after {
  position: absolute;
  top: 2px;
  right: .5em;
  transition: all .05s ease-in-out;
}

/* move unit more to the left on hover or focus within
   for arrow buttons will appear to the right of number inputs */
div:hover::after,
div:focus-within::after {
  right: 1.5em;
}

/* handle Firefox (arrows always shown) */
@supports (-moz-appearance:none) {
  div::after {
    right: 1.5em;
  }
}

/* set the unit abbreviation for each unit class */
.ms::after {
  content: 'ms';
}
.db::after {
  content: 'db';
}
.percent::after {
  content: '%';
}
<div class="ms">
  <input type="number" id="milliseconds" />
</div>
<hr />
<div class="db">
  <input type="number" id="decibel" />
</div>
<hr />
<div class="percent">
  <input type="number" id="percentages">
</div>

If you want to support browsers, that doesn't show these arrows at all, make use of @supports or media queries.


Another interesting approach would be to use a little of JavaScript in order to make suffix actually stick to the input text (which probably looks better):

const inputElement = document.getElementById('my-input');
const suffixElement = document.getElementById('my-suffix');


inputElement.addEventListener('input', updateSuffix);

updateSuffix();

function updateSuffix() {
  const width = getTextWidth(inputElement.value, '12px arial');
  suffixElement.style.left = width + 'px';
}


/**
 * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
 * 
 * @param {String} text The text to be rendered.
 * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
 * 
 * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
 */
function getTextWidth(text, font) {
    // re-use canvas object for better performance
    var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
    var context = canvas.getContext("2d");
    context.font = font;
    var metrics = context.measureText(text);
    return metrics.width;
}
#my-input-container {
  display: inline-block;
  position: relative;
  font: 12px arial;
}

#my-input {
  font: inherit;
}

#my-suffix {
  position: absolute;
  left: 0;
  top: 3px;
  color: #555;
  padding-left: 5px;
  font: inherit;
}
<div id="my-input-container">
  <input type="number" id="my-input" value="1500">
  <span id="my-suffix">ms.</span>
</div>

However, this is just a proof of concept. You will need to work on it a little further to make it production-ready, e.g. make it a reusable plugin.

Also, you will need to handle a case, where input element is getting overflowed.


I have a case where the design team wants the suffix to float with the values. We are using a custom font with very uneven number widths. I came with an idea to use a ghost to follow the input width and clamp the overflow with max-width by using a wrapper element. This is still a bit work in progress and glitchy (no initial fill, etc.).

const fillBuffer = (e) => {
  // Clear the buffer if input gets wiped
  if (e.target.value.length === 0) {
    e.target.parentElement.querySelector('.suffix span').textContent = "";
    return;
  }

  // Using a filler char will prevent the suffix to be overwritten with the input
  const extraFiller = e.target.value.length ? '1' : '';

  e.target.parentElement.querySelector('.suffix span').textContent = e.target.value + extraFiller;
}

// Attach the listeners
document.querySelectorAll('input').forEach((el) => {
  el.addEventListener('keydown', fillBuffer);
  el.addEventListener('keyup', fillBuffer);
});
* {
  font-size: 1em;
  font-family: Papyrus, sans-serif;
  box-sizing: border-box;
}

body {
  padding: 1em;
}

.input-wrapper {
  margin-bottom: 0.5em;
}

.input-wrapper.with-suffix {
  position: relative;
  max-width: 150px;
  overflow: hidden;
}

.input-wrapper.with-suffix input {
  margin: 0;
  width: 100%;
  outline: 0;
  padding-left: 4px;
  padding-right: 16px;
}

.input-wrapper.with-suffix .suffix {
  position: absolute;
  padding-left: 6px;
  top: 2px;
  pointer-events: none;
  width: 100%;
}

.input-wrapper.with-suffix .suffix span {
  user-select: none;
  pointer-events: none;
}

.input-wrapper.with-suffix .suffix .filler {
  display: inline-block;
  white-space: pre; /* Allow more than two whitespaces to be rendered */
  color: rgba(0, 0, 0, 0.0);
  background: rgba(255, 0, 0, 0.3);
  max-width: calc(100% - 16px);
}
<div class="input-wrapper with-suffix">
  <input type="text" value="5000">
  <div class="suffix"><span class="filler">5000</span><span>€</span></div>
</div>
  
  
<div class="input-wrapper with-suffix">
  <input type="text" value="5000">
  <div class="suffix"><span class="filler">5000</span><span>€</span></div>
</div>

If you have option to add elements to input then you can try this:

.container {
  max-width: 208px;    /*adjust it*/
  margin: auto;
  position: relative;
  display: inline-block;
}

#milliseconds {
  padding-right: 35px;
}

.ms {
  position: absolute;
  top: 0;
  right: 10px;
}
<div class="container">
  <input type="text" id="milliseconds">
  <span class="ms">ms</span>
</div>