· 7 min read

CSS field-sizing — Auto-resize Form Elements with a Single Line of CSS

This article was auto-translated from Chinese. Some nuances may be lost in translation.

With just a single line, CSS field-sizing: content allows a <textarea> to automatically resize its height based on its content—no JavaScript required. But before this property came along, accomplishing this was no easy task.

Listening to the input Event with JavaScript

The most common approach has been listening to the input event and dynamically adjusting the height via scrollHeight:

const textarea = document.querySelector('textarea')

textarea.addEventListener('input', () => {
  textarea.style.height = 'auto'
  textarea.style.height = `${textarea.scrollHeight}px`
})

The logic here is straightforward: first reset the height to auto so the browser recalculates scrollHeight, then assign the scrollHeight back to the element’s height. While this approach seems intuitive, it has several issues:

  1. Every keystroke triggers a reflow. Setting the height to auto and then setting it back forces the browser to recalculate the layout twice. This can easily cause noticeable layout jitter or flickering.
  2. Initial state must be handled manually. If the textarea already contains content when the page loads (such as in an edit mode), you have to trigger the calculation once by hand.
  3. Framework integrations are full of pitfalls. In React you need a ref combined with useEffect, in Vue you might need nextTick, and every framework handles it slightly differently.

In React, it would look like this:

function AutoResizeTextarea(props) {
  const ref = useRef<HTMLTextAreaElement>(null)

  useEffect(() => {
    const el = ref.current
    if (!el) return

    const resize = () => {
      el.style.height = 'auto'
      el.style.height = `${el.scrollHeight}px`
    }

    el.addEventListener('input', resize)
    resize() // Initialize

    return () => el.removeEventListener('input', resize)
  }, [])

  return <textarea ref={ref} {...props} />
}

Just to achieve “resizing based on content,” you have to create a component, manage refs, attach event listeners, and handle cleanup. And that’s not even counting scenarios where you need to enforce a maxHeight limit or support programmatic content updates.

Creating a Shadow Textarea

Another technique is creating a shadow textarea, meaning there are two <textarea> elements in the DOM. Their styles are kept identical, except the shadow textarea is hidden using visibility: hidden so it doesn’t appear on the screen.

<textarea                          // visible
  rows={minRows}
  style={style}
/>
<textarea                          // hidden shadow
  aria-hidden
  readOnly
  tabIndex={-1}
  style={{ visibility: 'hidden', position: 'absolute', overflow: 'hidden', height: 0 }}
/>

The main purpose is to prevent the visual flicker that occurs when toggling textarea between height: auto and height: ${scrollHeight}. The flow goes like this:

  • Apply the visible textarea’s calculated width (computedStyle.width) to the hidden textarea
  • Pass the visible textarea’s value to the hidden textarea
  • Read the hidden textarea’s scrollHeight
  • Apply that calculated height to the visible textarea

I remember people used to use a div as the shadow element even earlier, though I can no longer find the link. The premise was the same: using an off-screen element to prevent the visible textarea from flickering.

Here is the core logic, adapted from MUI’s TextareaAutosize:

function syncHeight(textarea, shadow, minRows) {
  const computedStyle = window.getComputedStyle(textarea);
  shadow.style.width = computedStyle.width;
  shadow.value = textarea.value || 'x';

  // textarea 如果最後一個字是換行,補一個空白避免高度計算不準
  if (shadow.value.endsWith('\n')) {
    shadow.value += ' ';
  }

  const contentHeight = shadow.scrollHeight;

  // 單行高度,用來計算 minRows
  shadow.value = 'x';
  const singleRowHeight = shadow.scrollHeight;

  const outerHeight = Math.max(minRows * singleRowHeight, contentHeight);
  textarea.style.height = `${outerHeight}px`;
}

A complete implementation also needs to handle ResizeObserver. In MUI’s source code, there is a special workaround: it disconnects the ResizeObserver while adjusting the height and reconnects it on the next frame to avoid triggering the "ResizeObserver loop completed with undelivered notifications" error. Just this single edge case shows how tedious it is to maintain auto-resizing in JavaScript.

field-sizing: content

Now, CSS natively provides the field-sizing property, allowing you to achieve this in a single line:

textarea {
  field-sizing: content;
}

That’s it. The browser will automatically adjust the size based on the form element’s content. If you want a default height, remember to set min-height or max-height. I usually like using rows to define height, but once field-sizing: content is applied, rows and cols no longer have any effect.

rows and cols attributes modify the default preferred size of a textarea. As a result, rows/cols have no effect on textarea elements with field-sizing: content set. MDN

When using field-sizing: content on its own, the element will expand indefinitely with its content. In practice, you’ll almost certainly pair it with min-width, max-width, min-height, and max-height to constrain the boundaries:

textarea {
  field-sizing: content;
  min-height: 3lh;
  max-height: 10lh;
  min-width: 200px;
  max-width: 100%;
}

I recommend pairing it with lh units, which makes your intent much clearer. lh represents the element’s own line-height. 3lh equals the height of three lines of text, which is far more semantic than px or em and less prone to layout shifts when the font size changes.

The behavior works as follows:

  • When the content is less than min-height, it stays at the minimum height.
  • When the content exceeds min-height but does not exceed max-height, the height expands with the content.
  • When the content exceeds max-height, the height locks at max-height and a scrollbar appears.

The width logic works the same way. Taking <input> as an example, it stays at min-width when there is little text, expands as you type, and stops growing once it hits max-width:

input {
  field-sizing: content;
  min-width: 100px;
  max-width: 400px;
}

Supported Elements

field-sizing isn’t limited to <textarea>; it also supports <input> and <select>:

/* The input auto-adjusts its width according to the length of the typed text */
input {
  field-sizing: content;
}

/* The select auto-adjusts its width according to the length of the selected option */
select {
  field-sizing: content;
}

Browser Support for field-sizing

As of April 2026, Chrome 123+, Edge 123+, and Opera 109+ support field-sizing. Safari has added support in its Technology Preview releases, and Firefox is actively working on it.1

Overall, mainstream browser support is catching up fast. However, if your product still needs to support older browsers, you will currently need to keep a JavaScript fallback in place. Whenever I implement this, I include a feature check to see if the browser supports it:

const isFieldSizingSupported = CSS.supports('field-sizing', 'content');

While branching your code conditionally can be a bit of a hassle, CSS saves a significant amount of unnecessary JavaScript, so I’ll adopt it whenever possible.

Summary

field-sizing: content solves a long-standing problem that front-end developers have wrestled with for over a decade. In the past, regardless of the framework you used, you were fundamentally synchronizing the content height manually in JavaScript. Now that browsers handle this natively, it saves not only lines of code, but also the mental overhead of maintaining all those workarounds.

It gives me the same feeling as when I first realized scroll-behavior: smooth could replace an entire smooth-scroll library with one line. As browsers gradually bake these common patterns into native features, developers can spend their time focusing on what truly matters.

Footnotes

  1. Can I Use ↩

Related Posts

Explore Other Topics