Holidays Deal Full Access for 50% OFF. Use code lex50 at checkout.

You'll get every theme available plus future additions. That's 43 themes total. Unlimited projects. Lifetime updates. One payment.

How to build an emoji picker textarea with Tailwind CSS and Alpine.js

Enhance any message box with a simple emoji picker powered by Alpine.js state and Tailwind CSS styling—no external libraries required.

Published on November 26, 2025 by Michael Andreuzza

This emoji picker pattern layers a clean textarea with a small row of emoji buttons. Alpine.js stores both the message text and the emoji list so you can append emojis with a single click, while Tailwind CSS handles spacing, focus states, and subtle shadows.

Why this UI works

  • A plain textarea keeps the UX familiar; emojis simply append to the current text.
  • Alpine’s reactive state means no DOM traversal—buttons just push strings into text.
  • The picker row fits inside any form because it’s just a flex container with gap spacing.
  • Ring/focus classes keep accessibility intact without adding extra JavaScript.

1. Alpine state wrapper

The component stores the current text plus an array of emoji options. You can swap the list for any category or fetch it dynamically.

<div x-data="{ text: '', emojis: ['💪', '🤣', '🔥', '👍', '🚀'] }" class="w-full space-y-2">
  <!-- textarea + picker -->
</div>
  • Keep text as a string so x-model stays in sync with the textarea.
  • Emojis live in an array so you can iterate with x-for.

2. Textarea styling and semantics

A simple label plus textarea keeps the markup accessible. Tailwind utilities add the soft border, blue focus ring, and smooth transitions.

<div>
  <label for="emoji-picker-textarea" class="text-sm font-medium text-zinc-500">Emoji picker</label>
  <textarea
    id="emoji-picker-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"
  ></textarea>
</div>
  • ring-1 ring-zinc-200 creates the faint outline, while focus:ring-2 focus:ring-blue-500 builds the active state.
  • Use rows="4" or any number to control default height.

3. Emoji button row

Render each emoji as a button so it is keyboard-focusable. Clicking appends the emoji to the existing text.

<div class="flex items-center gap-1">
  <template x-for="emoji in emojis" :key="emoji">
    <button
      type="button"
      class="flex items-center justify-center text-center shadow-subtle font-medium transition-colors duration-500 focus:outline-2 focus:outline-offset-2 size-8 p-0.5 text-xs rounded-md"
      @click="text += emoji"
      x-text="emoji"
    ></button>
  </template>
</div>
  • Keep the button label equal to the emoji so screen readers announce it; add aria-label if you need more clarity (e.g., “Add rocket emoji”).
  • shadow-subtle and size-8 keep the buttons compact.

4. Copy-and-paste snippet

Drop this anywhere inside an Alpine-enabled page.

<div
  class="w-full space-y-2"
  x-data="{ text: '', emojis: ['💪', '🤣', '🔥', '👍', '🚀'] }"
>
  <div>
    <label class="text-sm font-medium text-zinc-500"> Emoji picker </label>
    <textarea
      id="emoji-picker-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"
    ></textarea>
  </div>
  <div class="flex items-center mt-2 gap-1">
    <template x-for="emoji in emojis" :key="emoji">
      <button
        class="flex items-center justify-center text-center shadow-subtle font-medium duration-500 ease-in-out transition-colors focus:outline-2 focus:outline-offset-2 size-8 p-0.5 text-xs rounded-md"
        type="button"
        @click="text += emoji"
        x-text="emoji"
      ></button>
    </template>
  </div>
</div>

Finishing touches

  • Replace the emoji array with your own categories or fetch suggestions from an API and update emojis on the fly.
  • Debounce or watch text with Alpine’s $watch if you plan to sync it elsewhere.
  • Consider adding @keydown.enter.prevent on the textarea if you want to send messages immediately instead of newline insertion.

/Michael Andreuzza