How to build a character limit textarea with Tailwind CSS and Alpine.js
Guide writers with a live character counter and automatic ring warning when they exceed your threshold, built with Tailwind CSS and Alpine.js.
Published on November 26, 2025 by Michael AndreuzzaGive writers immediate feedback by showing how many characters they’ve typed and highlighting the textarea when they cross a limit. This component uses Alpine.js to track text length and Tailwind CSS to flip the focus ring to red once the threshold is exceeded.
Why it helps
- Copywriters see their remaining budget without modal popups or alerts.
x-modelkeeps the textarea reactive while a simpletext.lengthdrive both the counter and the red ring.- Tailwind’s
ringutilities create the warning state without extra CSS.
1. Alpine state and threshold
Store the message text plus a configurable limit. You can expose threshold as a prop if you reuse this component elsewhere.
<div class="w-full space-y-1" x-data="{ text: '', threshold: 50 }">
<!-- textarea + counter -->
</div>
- Start with
threshold: 50or any number that matches your product requirements. - Keep
textempty so the counter begins at zero.
2. Textarea binding + warning ring
Use x-model for the text and a dynamic class binding to change the ring when the user passes the limit.
<textarea
id="highlight-textarea"
x-model="text"
rows="4"
placeholder="Type your message"
:class="{ 'ring-red-500 focus:ring-red-500': text.length > threshold }"
class="block w-full px-4 py-2 text-sm text-blue-700 bg-white border border-transparent rounded-lg appearance-none duration-300 ring-1 ring-zinc-200 placeholder-zinc-400 focus:border-zinc-300 focus:bg-transparent focus:outline-none focus:ring-blue-500 focus:ring-offset-2 focus:ring-2"
></textarea>
- Keep the base ring styles in the static
class, then override with the conditional red ring. - You can add
maxlengthif you want to block further typing entirely, but the visual warning is often enough.
3. Counter display
A text row underneath shows the current count and the limit using Alpine’s templating.
<div class="mt-2 text-sm text-zinc-500">
Characters: <span x-text="text.length"></span> (Limit: <span x-text="threshold"></span>)
</div>
- Combine plain text with
x-textspans so the numbers update automatically. - If you want to show remaining characters, compute
threshold - text.lengthinstead.
4. Copy-and-paste snippet
Use the full markup below in any Alpine-enabled page.
<div class="w-full space-y-1" x-data="{ text: '', threshold: 50 }">
<label class="text-sm font-medium text-zinc-500">
Highlight excess characters
</label>
<textarea
id="highlight-textarea"
x-model="text"
rows="4"
placeholder="Type your message"
class="block w-full px-4 py-2 text-sm text-blue-700 bg-white border border-transparent rounded-lg appearance-none duration-300 ring-1 ring-zinc-200 placeholder-zinc-400 focus:border-zinc-300 focus:bg-transparent focus:outline-none focus:ring-blue-500 focus:ring-offset-2 focus:ring-2 sm:text-sm"
:class="{ 'ring-red-500 focus:ring-red-500': text.length > threshold }"
></textarea>
<div class="mt-2 text-sm text-zinc-500">
Characters: <span x-text="text.length"></span> (Limit:
<span x-text="threshold"></span>)
</div>
</div>
Finishing touches
- Surface the remaining count instead of total characters if that language resonates better (e.g., “12 characters left”).
- Trigger a
window.dispatchEventorx-onhook when the user hits the threshold to show additional warnings or disable submit buttons. - Tie
thresholdto a select dropdown if you support variable limits per field.
/Michael Andreuzza