# How to Build a Proper Auto-Growing Textarea

# The Problem With a Fixed Textarea

A normal `<textarea>` has a fixed height. When content overflows that height, the browser adds a vertical scrollbar. This is fine for long-form editing like a notes app. It is bad for prompt inputs, chat boxes, or comment fields where you want the user to see all of what they typed at all times without scrolling inside the element.

The goal is an element that starts at one line of height and grows downward as content fills it.

* * *

# Step 1. Start With the Right CSS

Before writing any JavaScript, you need to set up the textarea with CSS that allows it to grow.

```css
textarea {
  resize: none;
  overflow: hidden;
  min-height: 40px;
}
```

`resize: none` removes the drag handle that browsers add to the bottom-right corner of textareas. Without this, the user can manually drag the element to a fixed size, which fights against your auto-grow logic.

`overflow: hidden` is the most important rule here. When overflow is hidden, the browser does not add a scrollbar when content overflows. This has a side effect that makes the auto-grow technique possible: `scrollHeight` now correctly reports the full intrinsic height of the content, not just the visible portion. If overflow is not hidden, `scrollHeight` can return wrong or inconsistent values across browsers.

`min-height` sets the floor. The textarea will never shrink below this value even when empty.

* * *

# Step 2. Understand scrollHeight

`scrollHeight` is a DOM property on any element. It returns the total height of the element's content in pixels, including content that is not visible because it overflows. Think of it as the height the element would need to be to show all its content without any scrollbar.

For a textarea, `scrollHeight` includes the text, the line height, and the padding on top and bottom.

This is the number you want to assign to the element's height so it exactly fits its content.

* * *

# Step 3. The Core Resize Technique

The technique that actually works, consistently, across all browsers is this:

```javascript
function resize(el) {
  el.style.height = "auto";
  el.style.height = `${el.scrollHeight}px`;
}
```

Two lines. But the order matters and both lines are necessary.

**Why set height to** `auto` **first.**

If you just set `el.style.height = el.scrollHeight + 'px'` without resetting first, you get a bug when the user deletes text. The element already has an explicit pixel height set from the previous resize call. The browser uses that pixel height as the element's size when calculating `scrollHeight`. So `scrollHeight` reports the old height, not the new smaller one. The element never shrinks.

Setting height to `auto` first tells the browser to forget the explicit height and recalculate the element's natural height from its content. Now `scrollHeight` gives you the true intrinsic height. Then you set that as the explicit height.

Both assignments happen synchronously in the same function call. The browser does not repaint between them. So the user never sees the `auto` state. There is no flicker.

* * *

# Step 4. When to Call resize

You need to call `resize` every time the content changes. In plain JavaScript this is the `input` event.

```javascript
const textarea = document.querySelector("textarea");

textarea.addEventListener("input", () => {
  resize(textarea);
});
```

The `input` event fires after the value has changed, which means `scrollHeight` is already up to date when your function runs.

You also need to call `resize` once when the page loads if the textarea has a pre-filled value. Otherwise it will start at its CSS `min-height` even if the content is taller.

```javascript
// Call once on load for pre-filled values
resize(textarea);
```

* * *

# Step 5. Putting It Together in Plain JavaScript

```javascript
function resize(el) {
  el.style.height = "auto";
  el.style.height = `${el.scrollHeight}px`;
}

const textarea = document.querySelector("textarea");

// Resize once on load in case value is pre-filled
resize(textarea);

// Resize on every input event
textarea.addEventListener("input", () => resize(textarea));
```

```css
textarea {
  resize: none;
  overflow: hidden;
  min-height: 40px;
  max-height: 200px; /* optional ceiling */
}
```

If you want a ceiling on the height, add `max-height` in CSS. Once the content exceeds that height, the element stops growing and the overflow becomes scrollable. You will want to switch `overflow` to `auto` at that point so the scrollbar appears only when needed.

* * *

# Step 6. A Clean React Hook

In React you want the logic in a reusable hook so you can drop it into any component.

```typescript
import { useCallback, useEffect, useRef } from "react";

export function useAutoResize(value: string) {
  const ref = useRef<HTMLTextAreaElement>(null);

  const resize = useCallback(() => {
    const el = ref.current;
    if (!el) return;
    el.style.height = "auto";
    el.style.height = `${el.scrollHeight}px`;
  }, []);

  // Re-run every time the value changes
  useEffect(() => {
    resize();
  }, [value, resize]);

  return ref;
}
```

The hook takes `value` as a dependency for `useEffect`. Every time `value` changes, the effect runs and calls `resize`. This keeps the height perfectly in sync with the controlled value.

You use the hook like this:

```tsx
function CommentBox() {
  const [value, setValue] = useState("");
  const textareaRef = useAutoResize(value);

  return (
    <textarea
      ref={textareaRef}
      value={value}
      rows={1}
      onChange={(e) => setValue(e.target.value)}
      style={{ resize: "none", overflow: "hidden" }}
    />
  );
}
```

`rows={1}` sets the starting height to one line. Without this, the HTML default is `rows={2}`, so the element starts taller than one line even when empty.

* * *

# Step 7. Resetting Height After Submit

If you clear the value on submit, you also need to reset the height. Setting `value` to an empty string does trigger the `useEffect` because the value changed. The `resize` call will run and set the height back to the natural single-line height. This works automatically with the hook above as long as you clear value through state.

If you ever clear the textarea by directly mutating `el.value` instead of through React state, the effect will not run. Always go through state.

* * *

# Step 8. Handling Window Resize

When the window gets narrower, text reflows. Lines that fit on one line now wrap onto two. The height needs to update to match. The `input` event does not fire when the window resizes, so you need to handle this separately.

```typescript
useEffect(() => {
  window.addEventListener("resize", resize);
  return () => window.removeEventListener("resize", resize);
}, [resize]);
```

Add this inside the hook. Without it, the textarea height becomes wrong when the user resizes their browser window.

* * *

# Step 9. The Complete Hook With All Edge Cases

```typescript
import { useCallback, useEffect, useRef } from "react";

export function useAutoResize(value: string) {
  const ref = useRef<HTMLTextAreaElement>(null);

  const resize = useCallback(() => {
    const el = ref.current;
    if (!el) return;
    el.style.height = "auto";
    el.style.height = `${el.scrollHeight}px`;
  }, []);

  // Resize when value changes
  useEffect(() => {
    resize();
  }, [value, resize]);

  // Resize when window resizes (text reflow)
  useEffect(() => {
    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, [resize]);

  return ref;
}
```

* * *

# Summary of Why Each Piece Is There

| Piece | Why it is there |
| --- | --- |
| `overflow: hidden` | Makes `scrollHeight` accurate. Removes the scrollbar. |
| `resize: none` | Stops the user from manually overriding your height logic. |
| `el.style.height = 'auto'` | Clears the previous explicit height so `scrollHeight` reflects true content size. |
| `el.style.height = scrollHeight + 'px'` | Applies the correct height to the element. |
| `rows={1}` | Sets the single-line starting point. |
| `value` in `useEffect` deps | Triggers the resize on every content change. |
| `window resize` listener | Handles text reflow when viewport width changes. |

Every single piece has a specific reason. Remove any one of them and you get a bug.
