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 AndreuzzaThis 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
flexcontainer 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
textas a string sox-modelstays 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-200creates the faint outline, whilefocus:ring-2 focus:ring-blue-500builds 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-labelif you need more clarity (e.g., “Add rocket emoji”). shadow-subtleandsize-8keep 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
emojison the fly. - Debounce or watch
textwith Alpine’s$watchif you plan to sync it elsewhere. - Consider adding
@keydown.enter.preventon the textarea if you want to send messages immediately instead of newline insertion.
/Michael Andreuzza