17

I want to have an input whose width adapts to fit its content.

I'm trying to implement this answer to a similar question, but using React:

import React, { useState } from 'react';

export default () => {
  const [content, setContent] = useState('');
  const [width, setWidth] = useState(0);

  const changeHandler = evt => {
    setContent(evt.target.value);
  };

  return (
    <wrapper>
      <span id="hide">{content}</span>
      <input type="text" autoFocus style={{ width }} onChange={changeHandler} />
    </wrapper>
  );
};

The problem is I don't know how to then query the width of the span, in order to then change the width of the input (using setWidth).

How can I achieve this?

5 Answers 5

30

After a lot of fiddling around, I found a solution!

import React, { useState, useRef, useEffect } from 'react';

export default () => {
  const [content, setContent] = useState('');
  const [width, setWidth] = useState(0);
  const span = useRef();

  useEffect(() => {
    setWidth(span.current.offsetWidth);
  }, [content]);

  const changeHandler = evt => {
    setContent(evt.target.value);
  };

  return (
    <wrapper>
      <span id="hide" ref={span}>{content}</span>
      <input type="text" style={{ width }} autoFocus onChange={changeHandler} />
    </wrapper>
  );
};

To get a reference to the #hide span I employ useRef. Then, the width state variable can be updated via the function defined inside useEffect, which gets called everytime content changes.

I also had to switch the display: none in the css of #hide for position: absolute and opacity: 0, as otherwise targetRef.current.offsetWidth would always evaluate to 0.

Here's a working demo.

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

4 Comments

works with formik... decent hack...!!
nice one! working well for me in 2023. don't forget to also set pointer-events: none; in #hide's css
Nice! I found that with visibility: hidden; you don't need the opacity and z-index tricks. Unless I'm missing something...
Nice, worked for me. But you can significantly decrease component renders with using ref for input and updating its styles directly in useEffect like inputRef.current.style.width = ${spanRef.current.offsetWidth}px;
8

Well, this was interesting enough! I tried a few different ideas that I had, but none of them worked perfectly - especially not if they were to be written in a somewhat respectable code.

I found this post however and decided to try that out. https://stackoverflow.com/a/43488899/3293843

I am sure there are flaws with it, one for example is that it does act funny unless I use a monospaced font. But maybe there are some css tricks to get around that?

// Normally I'd go for ES6 imports, but to make it run as a StackOverflow snippet I had to do it this way
const { useState, useRef } = React;

const GrowingInput = () => {
  const [width, setWidth] = useState(0);
  
  const changeHandler = evt => {
    setWidth(evt.target.value.length);
  };
 
  return (
    <input style={{ width: width +'ch'}} type="text" autoFocus onChange={changeHandler} />
  )
};

const App = () => {
  return (
    <p>Lorem ipsum {<GrowingInput />} egestas arcu.</p>
  );
};

// Render it
ReactDOM.render(<App />, document.getElementById("react"));
input {
  font-family: Courier;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Have you considered using a contenteditable instead?

https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content

2 Comments

Check the solution I just posted (works with proportional fonts!)
That looks very similar to my first approach. The thing is that the reference was one click away affecting the input in a way that all characters were not 100% visible. I also had another test were the input would grow too long. Could you put it in a runable snippet? I was also not fond of positioning the span with absolute (which I also did)... maybe a portal is a better solution instead? But, if it works for you - that is great
4

Here is the simplest solution I've found.

You create a function that you'll use on change

   const handleChangeAndSize = (ev: ChangeEvent<HTMLInputElement>) => {
      const target = ev.target;
      target.style.width = '60px';
      target.style.width = `${target.scrollWidth}px`;

      handleChange(ev);
   };

Then you use it as a regular function in your component

<input type='text' onChange={handleChangeAndSize}/>

The style.width = 60px will allow to resize the input when shrinking, and the target.scrollWidth will watch the 'scrollable width' on x axis and set it as width.

Nb: credit to this guy: https://www.youtube.com/watch?v=87wfMZ56egU

1 Comment

This works well, except that the default width doesn't necessarily reflect the width of the content until the first time that it's manually adjusted.
2

Found out a trick using Refs in react.

style={{ width: inputRef.current ? inputRef.current.value.length + 'ch' : 'auto' }}

And set the ref={inputRef} for the element. Do remember to set the min-width for the input in your CSS.

2 Comments

It's not dynamic
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
0

Another option without using extra hidding element:

const GrowingInput = () => {
  const inputRef = React.useRef(null);
  
  const handleChange = () => {
    inputRef.current.style.width = "0";
    inputRef.current.style.width = `${inputRef.current.scrollWidth}px`;
  };
 
  return <input ref={inputRef} style={{width: 0}} autoFocus onChange={handleChange} />
};

const App = () => <p>Lorem ipsum {<GrowingInput />} egestas arcu. </p>;

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Comments

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.