# useAutosize

> Grows a textarea with what you write between its minimum and maximum height, animating each change without ever touching the text.

Docs: https://hextaui.com/docs/use-autosize
Markdown: https://hextaui.com/docs/use-autosize.md

```tsx title="components/examples/use-autosize/demo.tsx"
"use client"

import * as React from "react"

import { useAutosize } from "@/hooks/use-autosize"

export function UseAutosizeDemo() {
  const ref = React.useRef<HTMLTextAreaElement>(null)
  useAutosize(ref, true, undefined)

  return (
    <textarea
      ref={ref}
      rows={1}
      aria-label="Note"
      placeholder="Write a note. It grows up to six lines."
      className="max-h-[calc(6lh+1rem)] min-h-[calc(1lh+1rem)] w-full max-w-sm resize-none rounded-lg bg-muted px-3 py-2 text-sm/6 outline-none placeholder:text-muted-foreground focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
    />
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/use-autosize.json
```

This adds the hook and anything it depends on.

### Manual

Copy and paste the following code into your project.

```ts title="hooks/use-autosize.ts"
import * as React from "react"

import { easeOut, prefersReducedMotion } from "@/lib/motion"

function useAutosize(
  ref: React.RefObject<HTMLTextAreaElement | null>,
  enabled: boolean,
  value: unknown
) {
  const fitRef = React.useRef<((animate: boolean) => void) | null>(null)

  React.useLayoutEffect(() => {
    const textarea = ref.current
    if (!enabled || !textarea) {
      return
    }

    let animation: Animation | null = null
    let settled = 0
    let width = 0

    const fit = (animate: boolean) => {
      const from = animation
        ? parseFloat(getComputedStyle(textarea).height)
        : settled
      animation?.cancel()
      animation = null

      textarea.style.height = "auto"
      const style = getComputedStyle(textarea)
      const border =
        parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth)
      const max = parseFloat(style.maxHeight)
      const min = parseFloat(style.minHeight)
      const natural = textarea.scrollHeight + border
      const capped = Number.isFinite(max) ? Math.min(natural, max) : natural
      const target = Number.isFinite(min) ? Math.max(capped, min) : capped
      textarea.style.height = `${target}px`
      textarea.style.overflowY = natural > target + 1 ? "auto" : "hidden"
      settled = target

      if (
        animate &&
        from > 0 &&
        Math.abs(from - target) > 0.5 &&
        typeof textarea.animate === "function" &&
        !prefersReducedMotion()
      ) {
        const overflow = textarea.style.overflowY
        textarea.style.overflowY = "hidden"
        const running = textarea.animate(
          [{ height: `${from}px` }, { height: `${target}px` }],
          { duration: 180, easing: easeOut }
        )
        animation = running
        running.onfinish = () => {
          if (animation === running) {
            animation = null
            textarea.style.overflowY = overflow
          }
        }
      }
    }

    fitRef.current = fit
    fit(false)

    const onInput = (event: Event) => {
      if (event.target === textarea) {
        fit(true)
      }
    }
    window.addEventListener("input", onInput)
    const observer =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            if (textarea.clientWidth !== width) {
              width = textarea.clientWidth
              if (!animation) {
                fit(false)
              }
            }
          })
    observer?.observe(textarea)

    return () => {
      fitRef.current = null
      window.removeEventListener("input", onInput)
      observer?.disconnect()
      animation?.cancel()
      textarea.style.height = ""
      textarea.style.overflowY = ""
    }
  }, [enabled, ref])

  React.useLayoutEffect(() => {
    fitRef.current?.(true)
  }, [value])
}

export { useAutosize }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { useAutosize } from "@/hooks/use-autosize"
```

```tsx
const ref = React.useRef<HTMLTextAreaElement>(null)
useAutosize(ref, true, value)

<textarea
  ref={ref}
  rows={1}
  value={value}
  onChange={(event) => setValue(event.target.value)}
  className="min-h-[calc(1lh+1rem)] max-h-[calc(8lh+1rem)] resize-none"
/>
```

`<Textarea autoResize>` already uses this hook. Call it yourself when you render your own `<textarea>`, such as a chat composer or an inline editor.

## How it works

On each change the hook sets the height to `auto`, reads the natural `scrollHeight`, and clamps it to the element's own CSS `min-height` and `max-height`. All of that happens before paint, so you never see the collapsed frame. The bounds stay in your CSS, so they can be responsive or use `lh` to count lines.

```bash
min-h-[calc(2lh+1rem)]   at least two lines plus the vertical padding
max-h-[calc(8lh+1rem)]   then scroll after eight
max-h-64                 or any fixed length
```

- Height changes animate over 180ms, and a change that comes in mid-animation starts from the current height, so fast typing never jumps. Under reduced motion the height snaps.
- Past `max-height` the textarea scrolls. Otherwise its scrollbar stays hidden, so it doesn't flash on each new line.
- It refits when the width changes, because text that wraps differently changes the height.
- It listens for `input` on `window` and only measures. It never reads or writes the value, so keystrokes are never delayed or dropped.

## Examples

### Controlled values

Typing fires `input`, but setting `value` from code doesn't. Pass the value as the third argument and the textarea refits when you insert a template or clear it.

```tsx title="components/examples/use-autosize/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { useAutosize } from "@/hooks/use-autosize"

const template = `Hi team,

The release is out. Highlights:
- Faster search
- Offline drafts
- New keyboard shortcuts

Thanks!`

export function UseAutosizeControlled() {
  const ref = React.useRef<HTMLTextAreaElement>(null)
  const [value, setValue] = React.useState("")
  useAutosize(ref, true, value)

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <textarea
        ref={ref}
        rows={1}
        aria-label="Message"
        value={value}
        onChange={(event) => setValue(event.target.value)}
        placeholder="Message"
        className="max-h-[calc(8lh+1rem)] min-h-[calc(2lh+1rem)] w-full resize-none rounded-lg bg-muted px-3 py-2 text-sm/6 outline-none placeholder:text-muted-foreground focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
      />
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={() => setValue(template)}>
          Insert template
        </Button>
        <Button variant="ghost" size="sm" onClick={() => setValue("")}>
          Clear
        </Button>
      </div>
    </div>
  )
}
```

## Good to know

- Set `rows={1}` and `resize-none`. A manual resize handle fights the automatic height.
- Turning `enabled` off or unmounting removes the inline height and overflow, handing the size back to your CSS.
- On touch screens, use at least 16px text in the textarea, or iOS zooms in when it's focused.

## API reference

### useAutosize(ref, enabled, value)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `ref` | `RefObject<HTMLTextAreaElement \| null>` | – | The textarea to size. |
| `enabled` | `boolean` | – | Whether to size automatically. |
| `value` | `unknown` | – | The controlled value. Pass undefined for uncontrolled textareas. |

### Used by

`Textarea` and `InputGroupTextarea`.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
