The Quest For Perfect Freeform Input — Namchee
Back to Posts

The Quest For Perfect Freeform Input

9 minutes read

March 2025 has brought us a new update to the web platform: the induction of plaintext-only value for contenteditable attribute to Widely Available Baseline, which is a prevalent attribute commonly used in rich-text editors.

From what we can discern from the name alone, plaintext-only seems to strip down all rich-text formattings from values inside contenteditable into plaintexts.

However, won’t that defeat the purpose of contenteditable in the first place? Moreover, we already have good old <textarea> to handle freeform inputs. What is WHATWG trying to solve when introducting this new value?

When Textarea Isn’t Enough

Since its introduction of HTML2 in November 1995, freeform inputs has always been traditionally defined with the <textarea> element instead of <input>.

An example of textarea with minimal setup

The main difference between <textarea> and <input> element beside the wrapping behavior is the additional handle on the bottom right of the element for resizing and the addition of rows and cols attribute that controls the initial size of the element.

As you started adopting <textarea>, you will begin to realize that your styling options to the contents inside the <textarea> is only limited to inherited attributes that you can define in <textarea>.

Contents inside <textarea> are treated the same way as replaced elements, they have their own intrinsic dimensions and it’s up to the user agent to control how the contents are rendered.

Just like <input>, the user agent internally uses Shadow DOM to isolate the contents of <textarea> into its own DOM tree.

User agent Shadow DOM that encapsulate the contents of textarea (need to explicitly enable in DevTools)
User agent Shadow DOM that encapsulate the contents of textarea (need to explicitly enable in DevTools)

Since the contents largely exist outside the developer-controlled CSS rendering model, <textarea> lacked the native capability to automatically resize itself which is a common usecase for modern UI interfaces.

To address this issue, developers usually handles it with JavaScript in similar fashion as the following:

textarea.js

js

const textarea = document.querySelector('.textarea__autosize');

function resizeTextarea(el) {
  // Reset textarea height to auto to correctly calculate the new height
  el.style.height = 'auto';
  const newHeight = el.scrollHeight;

  el.style.height = `${newHeight}px`;
  el.style.overflowY = 'hidden';

  /**
   * If you want autosize to a certain max number of rows:
   *
   * const MAX_ROWS = 8;
   *
   * // Calculate maximum height from maxRows
   * const lineHeight =
   *   parseInt(window.getComputedStyle(el).lineHeight, 10) || 0;
   *
   * const maxHeight = lineHeight * maxRows;
   *
   * if (newHeight > maxHeight) {
   *   el.style.height = `${maxHeight}px`;
   *   el.style.overflowY = 'scroll';
   * }
   */
}

textarea.addEventListener('input', () => resizeTextarea(el));

While the autosizing implementation above works, it introduces 2 new problems:

  • Reliance to JavaScript, even though <textarea> works fine without it
  • Developers must manually wire autosizing logic to various HTML events like paste or direct value changes via .innerHTML. Otherwise, the <textarea> may not resize correctly, which might hurt the UX.

In the future, we might not need to manually handle autosizing in <textarea> due to the introduction of field-sizing. At the moment however, it’s only supported in Chromium-based browsers.

As for the list of other CSS features that don’t work at all in <textarea> without some wizardy due to Shadow DOM encapsulation:

  • ::before and ::after pseudo element
  • caret-color
  • Scrollbar-related styling prefixed with -webkit-scrollbar
  • CSS Highlight API

To address the issues with <textarea>, developers tend to replace their freeform inputs with a contenteditable element even when rich-text formatting isn’t needed.

However, many people who haven’t dealt with contenteditable before don’t realize that contenteditable comes with its own set of problems compared to <textarea> until they’ve delved too far with it.

Contenteditable: New Set of Problems

If you’re not living in a cave in the past decade, you might notice that some of the freeform input inside you favorite apps are capable of rendering wider variety of style and formats unlike what <textarea> can provide with plaintext.

Rich-text capabilities like that are commonly provided by contenteditable, which can usually be seen in discussion forums and social media platforms.

Bluesky post editor using contenteditable. Powered by ProseMirror
Bluesky post editor using contenteditable. Powered by ProseMirror

The attribute itself is surprisingly old, as it’s first introduced in July 2000 by Microsoft alongside designMode through Internet Explorer (RIP) 5.5.

The details were pretty lacking, but Ian Hickson wrote a WHATWG blogpost outlining the main purpose of these new attributes: extending the capability of <textarea> for rich-text formatting in general like WYSIWYG editors.

While designMode makes the entire document editable, contenteditable isolate the editability to a certain element. Usually, a <div> is used as a container for a freeform input defined with contenteditable.

An example of contenteditable div with minimal setup

Aside from rich-text formatting that <textarea> can’t do, you might realize that the container element automatically resize itself when the contents inside are mutated.

Unlike <textarea>, contenteditable doesn’t use Shadow DOM by default and its contents exist inside the normal DOM tree. As a result, it supports broader CSS features than <textarea>, such as those mentioned the previous section.

Due to its wider CSS support, it’s much easier to apply advanced styling to contenteditable without JavaScript, such as as limiting autosizing to a maximum number of lines until overflow with only pure CSS:

contenteditable.css

css

:root {
  /* Assume that we want a maximum of 8 lines before resizing */
  --maximum-lines: 8;
}

.contenteditable__autoresize {
  line-height: 1.5;
  min-height: calc(4rem + 16px);
  max-height: calc(var(--maximum-lines) * 1.5rem + 16px);
  overflow: auto;
  /* Ensure no width transition when scrollbar appears */
  scrollbar-gutter: stable;
}

/* Emulate <textarea> placeholder */
.contenteditable__autoresize:empty::before {
  content: attr(placeholder);
  opacity: 0.5;
  pointer-events: none;
}

On the surface, contenteditable seems like an upgrade to <textarea> with rich-text capabilities and broader CSS feature support. However, playing around contenteditable reveals plethora of problems that <textarea> doesn’t have:

  • contenteditable isn’t a form control element, which means no native <label> and placeholder support.
  • Interacting with contenteditable element as a form value requires JavaScript.
  • Accessibility need to be handled manually through ARIA attributes, which usually is forgotten or handled incorrectly.
  • Inconsistent many-to-one representation of rich-text to HTML elements, making it not well-behaved and hard to determine equality.
  • Invisible element such as additional <br /> in an empty contenteditable and occassional <span> must be taken into account before processing the contents.
  • Pasting contents to the element might cause unexpected result as foreign rich-text formats are included in the clipboard.
  • Various cross-browser behavior inconsistencies

To avoid frustration with contenteditable, seasoned developers usually just pick a mature library to handle rich-text editing such as CKEditor, ProseMirror, TinyMCE, any many more similar libraries.

Those who avoid libraries and care about consistency will spend most of their time building a model, rendering schema, and mapping rules for contenteditable — something small teams often can’t afford to do.

Plaintext-only Do Too Little

Going back to the previous section, one of the problems that contenteditable have is dealing with pasted inputs.

Since we don’t know the source of the clipboard, the pasted contents might misbehave due to containing rich-text elements that we don’t support.

Beside suggesting the users to use plaintext paste, there are 2 alternatives that can be used to solve this problem:

  • Re-map all supported rich-text elements to your content model and strip the rest of unsupported elements. This method requires more code.
  • Strip all rich-text elements, pasting them as plaintext.

Stripping rich-text elements from the clipboard before pasting can be done by listening to paste event in JavaScript:

Removing rich-text from paste event

js

const contenteditable = document.querySelector(
  '.contenteditable__autosize'
);

contenteditable.addEventListener('paste', (e) => {
  e.preventDefault();

  // remove text formatting from clipboard
  const pastedText = (e.clipboardData || window.clipboardData).getData('text/plain');

  // get the current caret position
  const selection = window.getSelection();
  if (!selection.rangeCount) {
    return;
  }

  // remove the selected text first!
  selection.deleteFromDocument();
  selection.getRangeAt(0).insertNode(document.createTextNode(pastedText));

  selection.collapseToEnd();
});

Recent development on web platform introduces us to plaintext-only value for contenteditable while the old behavior is usually referred as true.

What it basically do is it strips all rich-text formatting from the clipboard payload before applying the value to the input which you can try from the following element:

An example of contenteditable div, but plaintext

Contrary to its name, plaintext-only doesn’t remove rich-text capabilities from contenteditable. The feature is still supported as evidenced by rendering HTML elements inside contenteditable through .innerHTML.

While it does offer a no-code solution for rich-text clipboard problem, it doesn’t solve the meat of the problem with contenteditable: inconsistent rendering and native form control support.

Choosing Your Own Poison

We now have 2 alternatives to represent freeform input elements: <textarea> and contenteditable. But how should you determine which one to use?

Fortunately, choosing between 2 alternatives for freeform inputs can be simplified by answering the following question: do you need rich-text on your inputs?

If the answer is No, just use <textarea>. In my experience, contenteditable isn’t worth considering just for native autosize and some CSS feature support as you need to write a lot of extra code for contenteditable to behave itself.

If the answer is Yes, then ask yourself again: can you afford the additional development cost of using contenteditable considering its inherent problems?

If you can afford the cost, then design the rendering model, schema, and mapping while not forgetting about other quirks and custom features that you need.

If you cannot afford the cost, consider using a mature rich-text libraries and define your input limitations to the rich-text schema.

The only scenario where I think it’s okay to use a barebone contenteditable is when you’re building a throwaway code or building a simple proof-of-concept.

If you’re asking me whether plaintext-only changed anything, I’m going to say no. The paste behavior is only one of the smaller, easily solvable problems and doesn’t fix other problems with contenteditable.

Moreover, I find the name plaintext-only a bit misleading since it doesn’t transform contenteditable into a plaintext-only input, but it only provides a built-in handler for plaintext paste.

Final Thoughts

While it looks like I antagonized contenteditable too much, I don’t think that a vanilla contenteditable should be avoided at all cost as it’s the simplest way to define a rich-text editor on the web.

Choosing between <textarea> and contenteditable can be imagined as getting a meal. The former is akin to ordering a take-out, but your customization options are limited, maybe a few extra toppings or a different sauce.

The later is akin to buying raw ingredients and cooking food yourself without clear recipe. If you aren’t a chef or at least a home cook, there’s a good chance that you’ll mess something up along the way but at least you can do almost anything to it.

That being said, it’s important to use the right tool for the job and be conscious of it. If you don’t need rich-texts, use a <textarea>, else use a mature rich-text library and focus on other features instead of dealing with contenteditable quirks.

Share this Post

Tags

Web
CSS