To add a textarea to a form, use <textarea name="message"></textarea> inside the <form> with a <label> connected by for and id. Set rows for its height and maxlength to cap its length. It has no value attribute: any starting text goes between the tags. Style it with CSS using width: 100%, font: inherit and resize: vertical.
This guide covers every textarea attribute that matters, how to style it, how to make it grow as people type, and how to receive the message by email. It's part of the HTML form elements reference.
A basic textarea in a form
<form action="https://formsubmit.app/f/YOUR_FORM_ID" method="POST">
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" required></textarea>
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
<button type="submit">Send</button>
</form>The name is what gets submitted as the key, so name="message" sends message=.... Without a name, the browser skips the field entirely.
Textarea attributes
| Attribute | What it does |
|---|---|
name | The key the value is submitted under. Required for it to be sent. |
rows | Visible height in lines of text. Default is 2. |
cols | Visible width in average characters. Default is 20. Usually overridden by CSS width. |
placeholder | Hint text shown while empty. Not a replacement for a label. |
maxlength | Maximum number of characters. The browser stops input beyond it. |
minlength | Minimum number of characters. Checked on submit, only if the field isn't empty. |
required | The form won't submit while the textarea is empty. |
wrap | soft (default) or hard. Controls whether visual wraps become real line breaks on submit. |
readonly | Can't be edited, but is still submitted. |
disabled | Can't be edited and is not submitted. |
autocomplete | Usually off for free-text messages, or a value like street-address for an address box. |
spellcheck | true or false to toggle the browser spell checker. |
Combine minlength with required if you want both a non-empty message and a minimum length. minlength on its own lets an empty textarea through.
<textarea id="message" name="message" rows="6"
minlength="20" maxlength="2000" required></textarea>Why there's no value attribute
<input> is a void element and stores its starting value in value. <textarea> has a closing tag, and whatever sits between the tags is its starting value:
<textarea name="notes">Delivery after 5pm, please.</textarea>Two consequences catch people out:
- Whitespace counts. If you format your HTML with a line break and indentation between the tags, that whitespace becomes the default content, and the placeholder won't show. Keep the tags on one line:
<textarea name="notes"></textarea>. - In JavaScript, use
.value, not.textContentor.innerHTML. ChangingtextContentchanges the default, not what's currently in the box after someone has typed.
Label association
A textarea needs an accessible name, just like an input. Use a <label> with for matching the textarea's id. If you have help text, such as a length limit, connect it with aria-describedby so screen readers read it too:
<label for="message">Message</label>
<p id="message-hint">Up to 2,000 characters.</p>
<textarea id="message" name="message" rows="6" maxlength="2000"
aria-describedby="message-hint"></textarea>Styling a textarea with CSS
Browser defaults for textareas are small, use a monospace font in some browsers and ignore your page's font settings. A solid starting point:
textarea {
box-sizing: border-box; /* padding and border stay inside width: 100% */
width: 100%;
min-height: 8rem;
padding: 0.625rem 0.75rem;
font: inherit; /* use the page font, not the browser default */
line-height: 1.5;
color: inherit;
background: #fff;
border: 1px solid #c4c9d4;
border-radius: 0.5rem;
resize: vertical; /* taller, but never wider than the layout */
}
textarea:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
border-color: #4f46e5;
}
textarea:user-invalid {
border-color: #dc2626;
}Notes on each part:
box-sizing: border-boxstopswidth: 100%plus padding and border from overflowing its container.font: inheritfixes the mismatched font. Form controls don't inherit font settings by default.resizeacceptsnone,both,horizontalorvertical.verticalis the usual choice. Avoidnoneunless the box auto-grows, because people reviewing a long message benefit from making it taller.- Keep a visible focus style. If you remove the default
outline, replace it with something at least as clear. :user-invalidonly matches after the visitor has interacted with the field, so an empty required textarea isn't shown in red as soon as the page loads.
Auto-grow as the user types
With CSS: field-sizing
The field-sizing: content property makes a textarea size itself to its content, with no JavaScript:
textarea {
field-sizing: content;
min-height: 4lh; /* at least 4 lines */
max-height: 20lh; /* then scroll */
}Browser support isn't universal yet, so check current support before relying on it alone. Browsers that don't understand it ignore it and show a normal textarea, which is a safe fallback.
With JavaScript: a small fallback
For browsers without field-sizing, reset the height and set it to the content height on each input:
<textarea id="message" name="message" rows="3" data-autogrow></textarea>
<script>
if (!CSS.supports("field-sizing", "content")) {
document.querySelectorAll("textarea[data-autogrow]").forEach((el) => {
const fit = () => {
el.style.height = "auto";
el.style.height = el.scrollHeight + "px";
};
el.addEventListener("input", fit);
fit();
});
}
</script>Pair this with box-sizing: border-box from the CSS above, otherwise the height can be off by the border width.
Add a character counter
maxlength stops input at the limit but gives no warning. A live counter helps people plan their message. Use aria-live="polite" so screen readers announce it without interrupting.
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" maxlength="500"
aria-describedby="message-count"></textarea>
<p id="message-count" aria-live="polite">0 / 500</p>
<script>
const box = document.getElementById("message");
const count = document.getElementById("message-count");
box.addEventListener("input", () => {
count.textContent = `${box.value.length} / ${box.maxLength}`;
});
</script>Browsers count line breaks as one character for maxlength, although they're submitted as two (\r\n). If your server enforces an exact limit, allow a little slack.
How line breaks are submitted
When the form submits, every line break the visitor typed is sent as a CRLF pair (\r\n). Lines that only wrap visually because they're wider than the box are sent as one long line.
wrap="hard" changes that. With it, the browser inserts real line breaks wherever the text visually wraps, based on cols. It requires cols to be set and is rarely what you want for a message field, because the breaks depend on the box's width.
In a FormSubmit notification email, line breaks in the message are kept, so a multi-paragraph message reads as it was typed.
Send the textarea message to email
The textarea is often the whole point of a form: it's the message. To have that message arrive in your inbox, point the form's action at https://formsubmit.app/f/YOUR_FORM_ID with method="POST", as in the first example. Each submission is spam-filtered, stored in your dashboard and emailed to you, and replying answers the visitor directly when the form has an email field. The full setup is in how to send an HTML form to email, or start from a ready-made email form.
Related form elements
- HTML input types for single-line fields such as email and phone.
- HTML select dropdown for a fixed list of answers, such as a topic picker above the message.
- Fieldset and legend for grouping a message with related fields.
- Back to the HTML form elements overview.