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 Andreuzza · 3 min read

Give 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-model keeps the textarea reactive while a simple text.length drive both the counter and the red ring.
  • Tailwind’s ring utilities 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: 50 or any number that matches your product requirements.
  • Keep text empty 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 maxlength if 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-text spans so the numbers update automatically.
  • If you want to show remaining characters, compute threshold - text.length instead.

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.dispatchEvent or x-on hook when the user hits the threshold to show additional warnings or disable submit buttons.
  • Tie threshold to a select dropdown if you support variable limits per field.

/Michael Andreuzza