Auto-scaling input to width of value in React
Asked Answered
A

5

16

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?

Ankney answered 25/11, 2020 at 19:31 Comment(0)
A
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.

Ankney answered 26/11, 2020 at 14:30 Comment(4)
works with formik... decent hack...!!Heptamerous
nice one! working well for me in 2023. don't forget to also set pointer-events: none; in #hide's cssKloman
Nice! I found that with visibility: hidden; you don't need the opacity and z-index tricks. Unless I'm missing something...Predictory
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;Lan
K
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://mcmap.net/q/107711/-adjust-width-of-input-field-to-its-input

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

Karlenekarlens answered 25/11, 2020 at 21:50 Comment(2)
Check the solution I just posted (works with proportional fonts!)Ankney
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 greatKarlenekarlens
H
3

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

Hindustan answered 24/1, 2023 at 20:43 Comment(0)
D
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.

Diller answered 16/8, 2022 at 7:31 Comment(2)
It's not dynamicToilette
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.Shade
G
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>
Grandparent answered 26/6 at 16:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.