1

I wrote the following code to create a text input element that dynamically changes its width as input length increases, but it also has a minimum length:

var input = document.getElementById("entry");
input.oninput = resizeInput;
resizeInput.call(input);

 function resizeInput() {
    var len = this.value.length, minLen = 30, applied = Math.max(len, minLen);
    this.style.width = applied + "ch";
}
<input id="entry" type="text">

But if you insert a successive string of characters (iii.../7/f), you would notice that the input field width suddenly starts increasing at a very rapid pace as soon as the input length crosses the minimum length minLen. (Also, as pointed out in the comments, the issue does not occur for ggg.../m or others. I don't know why.)

I tried using the min-width property but to no avail.

var input = document.getElementById("entry");
input.oninput = resizeInput;
resizeInput.call(input);

function resizeInput() {
    this.style.width = this.value.length + "ch";
}
input { min-width: 30px !important; }
<input id="entry" type="text">

but it doesn't work either. So, what else do I do?

5
  • Isn't that the desired output? If it dynamically changes to the size of it's content, and you're rapidly adding content, shouldn't it grow quickly? Commented Jun 8, 2018 at 13:24
  • @DBS Why would I want miles of empty space following my text? :( I don't want any spacing to the right or the left, so, no, it isn't the desired output. Commented Jun 8, 2018 at 13:26
  • Hmm, I'm not seeing any additional white space in your example Commented Jun 8, 2018 at 13:26
  • 1
    @DBS Try typing in i vs m. OP is measuring in characters, but the default font is not a monospace font. That is the main issue... Commented Jun 8, 2018 at 13:29
  • @DBS Ah, this is interesting. The letter g doesn't cause any issue at all, rather f, 1, i, etc. do. This is strange. Commented Jun 8, 2018 at 13:30

4 Answers 4

4

The problem comes with the unit ch which takes the height as base. Since not all characters have the same height as width it might end up with a margin on the right (for instance by using i). https://css-tricks.com/the-lengths-of-css/

You could solve the font issue by either using a monospace font or a contentEditable element, since it kinda resizes itself.

p{
  border: 1px solid #000;
  display: inline-block;
  min-width: 150px
}
<p contentEditable = 'true'></p>

Depending on what you want you would have to change the css or script to prevent pasting html or linebreaks.

Another option to keep the integrity of the input element would be by using css ::after to set the width for you:

.inputText{
  display: inline-block;
}
.inputText > input[type=text]{
  font-size: 13px;
  font-family: arial;
  width: 100%
}

.inputText::after{
  background:red;
  border: 1px solid black;
  content: attr(data-content);
  display: block;
  font-family: arial;
  font-size: 13px;
  visibility: hidden
}
<div class = 'inputText' data-content = 'text'>
  <input type = 'text' placeholder = 'text1' oninput = 'parentNode.setAttribute("data-content", this.value)' />
</div>
<div class = 'inputText' data-content = 'text'>
  <input type = 'text' placeholder = 'text2' oninput = 'parentNode.setAttribute("data-content", this.value)' />
</div>

Update

Created a little plugin for it, which does not handle all cases yet the basic ones. Also, I replaced div with label since those are more suitable for input elements. Theoretically, it could be expanded.

;(function(ns){
  "use strict";

  //REM: Makes an input-element flexible to its width
  function _makeFlex(input){
    if(input && input.type){
      //REM: Wrapping the input-element with a label-element (makes the most sense on inputs)
      _wrapInputInLabel(input);

      //REM: Adding a listener to inputs, so that the content of othe ::after can be adjusted
      input.addEventListener('input', _onInput, false);
    }
  };

  function _onInput(){
    var tInput = this,
      tParent = tInput.parentNode;

    //REM: Just verifying.. better save than sorry :-)
    if(tInput.type && tParent.tagName === 'LABEL' && tParent.getAttribute('data-flex')){
      //REM: Here exceptions can be set for different input-types
      switch(tInput.type.toLowerCase()){
        case 'password':
          //REM: This one depends on the browser and/or OS
          //https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/password
          tParent.setAttribute('data-flex-content', tInput.value.replace(/./g, '•'))
          break
        default:
          tParent.setAttribute('data-flex-content', tInput.value)
      }
    }
  };

  //REM: Wraps the input in a label-element
  function _wrapInputInLabel(input){
    if(input){
      var tLabel = (input.parentNode && input.parentNode.tagName === 'LABEL') ? input.parentNode : input.parentNode.appendChild(document.createElement('label'));
      tLabel.setAttribute('data-flex', input.getAttribute('data-flex'));
      tLabel.appendChild(input);

      //REM: Copy the font-styles on the label - can be expanded with more
      tLabel.style.fontFamily = window.getComputedStyle(input, null).getPropertyValue('font-family');
      tLabel.style.fontSize = window.getComputedStyle(input, null).getPropertyValue('font-size');

      if(input.id){
        tLabel.setAttribute('for', input.id)
      }
    }
  };

  ns.Flex = {
    Init: function(){
      //REM: Loops through all the marked input elements
      for(let tL=document.querySelectorAll('input[data-flex]'), i=0, j=tL.length; i<j; i++){
        _makeFlex(tL[i])
      }
    }
  }
})(window.mynamespace=window.mynamespace || {});

;window.onload = function(){
  mynamespace.Flex.Init()
};
label[data-flex]{
  font-family: arial;
  font-size: 13px;
  display: inline-block;
  min-width: 30px;
  position: relative
}

label[data-flex]::after{
  background: red;
  border: 1px solid black;
  content: attr(data-flex-content);
  display: block;
  visibility: hidden
  /*REM: following styles are just for demo purposes */
  line-height: 20px;
  visibility: visible;
}

/*REM: Has a little slider on the right */
label[data-flex='number']::after{
  margin-right: 18px
}

label[data-flex] > input{
  font-size: 13px;
  font-family: arial;
  width: 100%
}
<input type = 'text' placeholder = 'text' data-flex = 'flex' id = 'inText' />
<input type = 'text' placeholder = 'crazy' data-flex = 'flex' id = 'inText2' style = 'font-size: 20px; font-family: comic-sans' />
<input type = 'password' placeholder = 'password' data-flex = 'flex' id = 'inPassword' />

<label>
  <input type = 'number' placeholder = 'number' data-flex = 'number' id = 'inNumber' />
</label>

Another update

Was researching and trying a bit further and ran into a plugin that tries to do something alike.

I extracted and changed the part for the input-elements a bit. For me in Chrome all input types work fine. IE11 requires a polyfill for scrollWidth on input-elements and Firefox has issues with the type number. Yet I guess from the idea using the clientWidth and scrollWidth this is - in theory - the best solution:

//REM: Resizes the Input-Element
function resizeInput(input){
  if(input.nodeName.toLowerCase() === 'input'){
    var tStyle = getComputedStyle(input),
        tOffset = 0;

    //REM: Input-Elements with no width are likely not visible
    if(input.getBoundingClientRect().width){
      //REM: Resetting the width for a correct scroll and client calculation
      input.style.width = '0';

      //REM: Calculting the offset
      switch(tStyle.boxSizing){
        case 'padding-box':
          tOffset = input.clientWidth;
          break
        case 'content-box':
          tOffset = parseFloat(tStyle.minWidth);
          break
        case 'border-box':
          tOffset = input.offsetWidth;
          break
      };

      //REM: Somehow IE11 does not seem to support scrollWidth properly
      //https://github.com/gregwhitworth/scrollWidthPolyfill
      var tWidth = Math.max(tOffset, input.scrollWidth - input.clientWidth);

      input.style.width = tWidth + "px";

      //REM: This is kind of a double-check to backtrack the width by setting an unlikely scrollLeft
      for(var i=0; i<10; i++){
        input.scrollLeft = 1000000;

        if(input.scrollLeft === 0){
          break;
        };

        tWidth += input.scrollLeft;
        input.style.width = tWidth + 'px'
      }
    }
    else{
      //REM: Input-Element is probably not visible.. what to do? >.<
      element.style.width = element.value.length + 'ch'
    }
  }
};
input{
  min-width: 30px
}
<input type = 'text' placeholder = 'text' oninput = 'resizeInput(this)' />
<input type = 'number' placeholder = '99' oninput = 'resizeInput(this)' />
<input type = 'password' placeholder = 'password' oninput = 'resizeInput(this)' />

Sign up to request clarification or add additional context in comments.

5 Comments

Problem with this solution is that you can't take advantage of input's attributes like required or pattern and it also doesn't work for anything but text (think password, email, number). All these features would have to be added using extra JS.
Yes, that is correct. It also has problems with pasted HTML of course, unless you solve it in a script - like I wrote in my answer.
@user3210641: I found a solution which keeps the advantages of the input, yet I could not get it working with other input types yet - for instance password has other characters and hence a different length :-)
The second edit is interesting! Why do you have two inputs in it though?
@Gaurang Tandon: Just as an example. Was playing with different input types. It is kinda in progress, sadly no time at the moment :-)
2

Although @Lain's answer is the simplest solution it has multiple pitfalls - if you don't want to support multiline input you have listen for line breaks as well as pasting of text and it also doesn't support input's native attributes like type, required, pattern etc.

What you can do instead is create a container for your input and hide 'helper' element under your input field. Then you update your input element's width based on the width of your helper element.

EDIT: As mentioned by @Mr.Polywhirl my previous solution wouldn't work if you directly changed input's font-family or font-size. Below is updated solution which solves this issue:

// Save reference of the elements
const input = document.getElementById('input-field');
const helper = document.getElementById('input-text');

// Credit : https://stackoverflow.com/questions/7444451/how-to-get-the-actual-rendered-font-when-its-not-defined-in-css/7444724#7444724
const css = (element, property) => window.getComputedStyle(element, null).getPropertyValue(property);

// Listen to changes on input element
input.addEventListener('input', function(event) {
  // Save input current value
  const value = event.target.value;

  // Get current font styles of the input and set them to helper 
  helper.style.font = css(input, 'font');

  // Update helper text according to the input value
  helper.innerText = value;

  // Update input width to match helper width
  input.style.width = css(helper, 'width');
});
/* Necessary for helper position */
.input-container {
  position:relative;
}

.input-field {
  /* Set initial width */
  width: 5rem;

  /* Set minial width */
  min-width: 5rem;
}

/* Hide helper */
.input-text {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -999;
}
<div class="input-container">
  <input type="text" class="input-field" id="input-field" />
  <span class="input-text" id="input-text"></span>
</div>

5 Comments

This seems similar to syntax-highlighting code editors. Where they have a backing text area, but render styled divs in top. The only problem is if you change the font style of the input, you need to apply the same style to the helper span/div. It's a neat little trick.
Hm good point ! I didn't think of that ... Edited my answer so it is independent on the container's font-size and font-family, but rather computes current font styles from the input element.
Maybe it is my browser (Chrome) yet it does not work for me. The input does not extend.
The input doesn't extend for me too.
in which way does your solution support different input types better than the other two solutions? for me just text works with this one aswell.
1

In the modern spec, CSS has addressed this need and there is now a way to enable dynamic sizing with just a single line of CSS using field-sizing property:

input, textarea {
    field-sizing: content;
}

The desired min and max growth sizes can be then be fine-tuned using min-inline-size and min-block-size.

However, note that only chromium-based browsers have adopted this since 2024, so if necessary, Firefox and Safari will need some fallbacks.


caniuse: https://caniuse.com/?search=field-sizing
further reading: https://developer.chrome.com/docs/css-ui/css-field-sizing

Comments

1

You can dynamically grab the computed font and then measure the text using HTML5 canvas.

You are trying to measure in characters, but the font is not always a monospace font. You can test this by typing in i vs m in your example. You need to measure the entire text and consider the width of each individual character.

var input = document.getElementById('entry'); // Grab the element
input.oninput = resizeInput;                  // Add listener
resizeInput.call(input);                      // Trigger change,

function resizeInput() {
  var fontFamily = css(this, 'font-family');
  var fontSize   = css(this, 'font-size');
  var fontStyle  = css(this, 'font-style');

  this.style.width = getTextWidth(this.value, fontSize + ' ' + fontFamily) + 'px';
}

// https://stackoverflow.com/a/7444724/1762224
function css(element, property) {
  return window.getComputedStyle(element, null).getPropertyValue(property);
}

// https://code.i-harness.com/en/q/1cde1
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;
}
input {
  min-width: 30px !important;
}
<input id="entry" type="text">

5 Comments

But how do you measure the text in relation to the font family and size? Is there another way to call context.measureText(text) without using canvas?
cool solution. but i join user3210641, the other solution looks more simple without any calculations needed.
But then you are destroying the integrity of the <input> field. You can no longer utilized built in functionality, it is just a paragraph element...
Check my answer below.
you are right with the integrity of course. i just like the simplicity of the other solution. just my personal taste.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.