26

What I'm trying to achieve is a textarea that starts out as a single line but will grow up to 4 lines and at that point start to scroll if the user continues to type. I have a partial solution kinda working, it grows and then stops when it hits the max, but if you delete text it doesn't shrink like I want it to.

This is what I have so far.

export class foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      textareaHeight: 38
    };
  }

  handleKeyUp(evt) {
    // Max: 75px Min: 38px
    let newHeight = Math.max(Math.min(evt.target.scrollHeight + 2, 75), 38);
    if (newHeight !== this.state.textareaHeight) {
      this.setState({
        textareaHeight: newHeight
      });
    }
  }

  render() {
    let textareaStyle = { height: this.state.textareaHeight };
    return (
      <div>
        <textarea onKeyUp={this.handleKeyUp.bind(this)} style={textareaStyle}/>
      </div>
    );
  }
}

Obviously the problem is scrollHeight doesn't shrink back down when height is set to something larger. Any suggestion for how I might be able to fix this so it will also shrink back down if text is deleted?

jcmitch
  • 2,088
  • 9
  • 27
  • 33

10 Answers10

43

ANOTHER SIMPLE APPROACH (without an additional package)

export class foo extends React.Component {
  handleKeyDown(e) {
    e.target.style.height = 'inherit';
    e.target.style.height = `${e.target.scrollHeight}px`; 
    // In case you have a limitation
    // e.target.style.height = `${Math.min(e.target.scrollHeight, limit)}px`;
  }

  render() {
    return <textarea onKeyDown={this.handleKeyDown} />;
  }
}

The problem when you delete the text and textarea doesn't shrink back is because you forget to set this line

e.target.style.height = 'inherit';

Consider using onKeyDown because it works for all keys while others may not (w3schools)

In case you have padding or border of top or bottom. (reference)

handleKeyDown(e) {
    // Reset field height
    e.target.style.height = 'inherit';

    // Get the computed styles for the element
    const computed = window.getComputedStyle(e.target);

    // Calculate the height
    const height = parseInt(computed.getPropertyValue('border-top-width'), 10)
                 + parseInt(computed.getPropertyValue('padding-top'), 10)
                 + e.target.scrollHeight
                 + parseInt(computed.getPropertyValue('padding-bottom'), 10)
                 + parseInt(computed.getPropertyValue('border-bottom-width'), 10);

    e.target.style.height = `${height}px`;
}

I hope this may help.

Tatsu
  • 1,743
  • 16
  • 15
29

you can use autosize for that

LIVE DEMO

import React, { Component } from 'react';
import autosize from 'autosize';

class App extends Component {
    componentDidMount(){
       this.textarea.focus();
       autosize(this.textarea);
    }
    render(){
      const style = {
                maxHeight:'75px',
                minHeight:'38px',
                  resize:'none',
                  padding:'9px',
                  boxSizing:'border-box',
                  fontSize:'15px'};
        return (
          <div>Textarea autosize <br/><br/>
            <textarea
            style={style} 
            ref={c=>this.textarea=c}
            placeholder="type some text"
            rows={1} defaultValue=""/>
          </div>
        );
    }
}

or if you prefer react modules https://github.com/andreypopp/react-textarea-autosize

Kokovin Vladislav
  • 10,241
  • 5
  • 38
  • 36
14

Just use useEffect hook which will pick up the height during the renderer:

import React, { useEffect, useRef, useState} from "react";
const defaultStyle = {
    display: "block",
    overflow: "hidden",
    resize: "none",
    width: "100%",
    backgroundColor: "mediumSpringGreen"
};

const AutoHeightTextarea = ({ style = defaultStyle, ...etc }) => {
    const textareaRef = useRef(null);
    const [currentValue, setCurrentValue ] = useState("");// you can manage data with it

    useEffect(() => {
        textareaRef.current.style.height = "0px";
        const scrollHeight = textareaRef.current.scrollHeight;
        textareaRef.current.style.height = scrollHeight + "px";
    }, [currentValue]);

    return (
        <textarea
            ref={textareaRef}
            style={style}
            {...etc}
            value={currentValue}

            onChange={e=>{
            setCurrentValue(e.target.value);
            //to do something with value, maybe callback?
            }}
        />
    );
};

export default AutoHeightTextarea;
General Grievance
  • 4,555
  • 31
  • 31
  • 45
  • You should add an explanation to accompany your post. Posts with just code without explanation tend to be frowned upon :) – MBorg Mar 22 '20 at 05:19
10

Really simple if you use hooks "useRef()".

css:

.text-area {
   resize: none;
   overflow: hidden;
   min-height: 30px;
}

react componet:

export default () => {
 const textRef = useRef<any>();

 const onChangeHandler = function(e: SyntheticEvent) {
  const target = e.target as HTMLTextAreaElement;
  textRef.current.style.height = "30px";
  textRef.current.style.height = `${target.scrollHeight}px`;
 };

 return (
   <div>
    <textarea
      ref={textRef}
      onChange={onChangeHandler}
      className="text-area"
     />
    </div>
  );
};
sajan
  • 1,355
  • 1
  • 14
  • 19
5

you can even do it with react refs. as setting ref to element

<textarea ref={this.textAreaRef}></textarea> // after react 16.3
<textarea ref={textAreaRef=>this.textAreaRef = textAreaRef}></textarea> // before react 16.3

and update the height on componentDidMount or componentDidUpdate as your need. with,

if (this.textAreaRef) this.textAreaRef.style.height = this.textAreaRef.scrollHeight + "px";
Yash Ojha
  • 792
  • 9
  • 17
3

actually you can get out of this with useState and useEffect

function CustomTextarea({minRows}) {
  const [rows, setRows] = React.useState(minRows);
  const [value, setValue] = React.useState("");
  
  React.useEffect(() => {
    const rowlen = value.split("\n");

    if (rowlen.length > minRows) {
      setRows(rowlen.length);
    }
  }, [value]);

  return (
    <textarea rows={rows} onChange={(text) => setValue(text.target.value)} />
  );
}

Uses

<CustomTextarea minRows={10} />
1
import { useRef, useState } from "react"
const TextAreaComponent = () => {
  const [inputVal, setInputVal] =useState("")
  const inputRef = useRef(null)



  const handleInputHeight = () => {
const scrollHeight = inputRef.current.scrollHeight;
inputRef.current.style.height = scrollHeight + "px";
 };

 const handleInputChange = () => {
  setInputVal(inputRef.current.value)
  handleInputHeight()
 }
 
 return (
    <textarea 
     ref={inputRef}  
     value={inputVal}
     onChange={handleInputChange} 
      onKeyDown={(e) => {
          if (e.key === "Enter") {
            handleSubmit(e);
            inputRef.current.style.height = "40px";
          }
        }}
     />
  )}
codeXpath
  • 11
  • 2
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 03 '22 at 11:53
1

Extremely simple solution:

function allowTextareasToDynamicallyResize() {
  let textareas = document.getElementsByTagName('textarea');
  for (let i = 0; i < textareas.length; i++) {
    textareas[i].style.height = textareas[i].scrollHeight + 'px';
    textareas[i].addEventListener('input', (e) => {
      e.target.style.height = textareas[i].scrollHeight + 'px';
    });
  }
}

// Call this function in the componentDidMount() method of App, 
// or whatever class you want that contains all the textareas 
// you want to dynamically resize.

This works by setting an event listener to all textareas. For any given textarea, new input will trigger a function that resizes it. This function looks at any scrollHeight, i.e. the height that is overflowing out of your existing container. It then increments the textarea height by that exact height. Simple!

As mentioned in the comment, you have to call this function in some method, but the important part is that you call it AFTER everything is mounted in React / populated in JS. So the componentDidMount() in App is a good place for this.

0

I like using this.yourRef.current.offsetHeight. Since this is a textarea, it wont respond to height:min-content like a <div style={{height:"min-content"}}>{this.state.message}</div> would. Therefore I don't use

uponResize = () => {
 clearTimeout(this.timeout);
  this.timeout = setTimeout(
   this.getHeightOfText.current &&
   this.setState({
    heightOfText: this.getHeightOfText.current.offsetHeight
   }),
  20
 );
};
componentDidMount = () => {
 window.addEventListener('resize', this.uponResize, /*true*/)
}
componentWillUnmount = () => {
 window.removeEventListener('resize', this.uponResize)
}

but instead use

componentDidUpdate = () => {
 if(this.state.lastMessage!==this.state.message){
  this.setState({
   lastMessage:this.state.message,
   height:this.yourRef.current.offsetHeight
  })
 }
}

on a hidden div

<div
 ref={this.yourRef}
 style={{
  height:this.state.height,
  width:"100%",
  opacity:0,
  zIndex:-1,
  whiteSpace: "pre-line"
 })
>
 {this.state.message}
</div>
0

Using hooks + typescript :

import { useEffect, useRef } from 'react';
import type { DetailedHTMLProps, TextareaHTMLAttributes } from 'react';

// inspired from : https://stackoverflow.com/a/5346855/14223224
export const AutogrowTextarea = (props: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) => {
  const ref = useRef<HTMLTextAreaElement>(null);
  let topPadding = 0;
  let bottomPadding = 0;

  const resize = () => {
    ref.current.style.height = 'auto';
    ref.current.style.height = ref.current.scrollHeight - topPadding - bottomPadding + 'px';
  };

  const delayedResize = () => {
    window.setTimeout(resize, 0);
  };

  const getPropertyValue = (it: string) => {
    return Number.parseFloat(window.getComputedStyle(ref.current).getPropertyValue(it));
  };

  useEffect(() => {
    [topPadding, bottomPadding] = ['padding-top', 'padding-bottom'].map(getPropertyValue);

    ref.current.focus();
    ref.current.select();
    resize();
  }, []);

  return <textarea ref={ref} onChange={resize} onCut={delayedResize} onPaste={delayedResize} onDrop={delayedResize} onKeyDown={delayedResize} rows={1} {...props} />;
};
justcodin
  • 857
  • 7
  • 17