Tutorials / / 6 min read
How to create a chat bubble with Tailwind CSS: JavaScript and Alpine.js
Build a chat bubble that toggles from a floating button with Tailwind CSS, two ways: vanilla JavaScript class toggling or Alpine.js transitions.
Today we are building a chat bubble: the little floating button that opens a contact panel. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a chat or contact bubble?
A chat bubble is a small panel that appears when you toggle a floating button, the one that usually says “Chat”, “Help” or “Contact”. It normally sits at the bottom right of the screen and contains a form that lets the user send a message.
Use cases
- Customer support: let users start a real-time conversation with a support representative.
- Sales assistance: engage visitors with product questions to drive conversions.
- Feedback collection: gather suggestions or issues without sending people to a separate form.
- Lead generation: capture contact details through a friendly prompt.
The markup
id="app": the root element of the component.id="modal": the panel that holds the chat bubble content and the form.hiddenandflex-col: the visibility toggle; the script swapshiddenforflexwhen opening.fixed z-50 bottom-[100px] top-0 left-0 sm:top-auto sm:right-5 sm:left-auto: pins the panel to the viewport, full width on mobile and bottom right on larger screens. Thebottom-[100px]value is custom, just use what you want.h-[calc(100%-95px)] w-full sm:w-[350px] min-h-[250px] sm:h-[600px] overflow-auto: sizing, with scrolling in case the content is too long.id="toggleButton": the floating button that toggles the chat bubble.id="openIcon"andid="closeIcon": the two icons that swap depending on the state.
The rest of the classes are visual styles.
Classes and irrelevant content are removed for brevity, but I’ll keep those classes relevant to the tutorial.
<div id="app">
<div
id="modal"
class="hidden fixed flex-col z-50 bottom-[100px] top-0 ring-0 left-0 sm:top-auto sm:right-5 sm:left-auto h-[calc(100%-95px)] w-full sm:w-[350px] overflow-auto min-h-[250px] sm:h-[600px] bg-background shadow-2xl rounded-xl transition-opacity duration-200 ease transform"
>
<div>
<div>
<div>
<img src="..." alt="#" />
<img src="..." alt="#" />
<img src="..." alt="#" />
<img src="..." alt="#" />
</div>
</div>
<h3>Got any questions?</h3>
<p>Typically replies under 47 minutes</p>
</div>
<div>
<div>
<img src="..." alt="" />
<div>
<div>
<p>
Hey, How are can we help you? We recommend to checkout our
documentation and get started, we don't really know much anyways.
</p>
</div>
<span>2 min ago</span>
</div>
</div>
<div>
<input type="text" placeholder="Type your message…" />
</div>
</div>
</div>
<button id="toggleButton">
<svg id="openIcon">
<!-- Svg content for the chat bubble icon goes here-->
</svg>
<svg id="closeIcon">
<!-- Svg content for the close bubble icon goes here-->
</svg>
</button>
</div>The script
document.addEventListener("DOMContentLoaded", function () {: waits for the page, then grabsmodal,toggleButton,openIconandcloseIcon.toggleButton.addEventListener("click", ...): checksmodal.classList.contains("hidden")to know the current state.- Opening: removes
hidden, addsflex, then a 10mssetTimeoutremovesopacity-0andtranslate-y-5so the transition can play; the icons swap. - Closing: adds
opacity-0andtranslate-y-5first, then a 200mssetTimeout, matching the transition duration, addshiddenback and removesflex; the icons swap back. - The
documentclick listener closes the panel the same way when the click lands outside both the modal and the toggle button.
document.addEventListener("DOMContentLoaded", function () {
var modal = document.getElementById("modal");
var toggleButton = document.getElementById("toggleButton");
var openIcon = document.getElementById("openIcon");
var closeIcon = document.getElementById("closeIcon");
toggleButton.addEventListener("click", function () {
var isHidden = modal.classList.contains("hidden");
if (isHidden) {
modal.classList.remove("hidden");
modal.classList.add("flex");
setTimeout(function () {
modal.classList.remove("opacity-0", "translate-y-5");
}, 10); // timeout for transition effect
openIcon.classList.add("hidden");
closeIcon.classList.remove("hidden");
} else {
modal.classList.add("opacity-0", "translate-y-5");
setTimeout(function () {
modal.classList.add("hidden");
modal.classList.remove("flex");
}, 200); // match transition duration
openIcon.classList.remove("hidden");
closeIcon.classList.add("hidden");
}
});
document.addEventListener("click", function (event) {
if (!modal.contains(event.target) && !toggleButton.contains(event.target)) {
modal.classList.add("opacity-0", "translate-y-5");
setTimeout(function () {
modal.classList.add("hidden");
modal.classList.remove("flex");
}, 200);
openIcon.classList.remove("hidden");
closeIcon.classList.add("hidden");
}
});
});The Alpine.js version
Same component, no separate script: the state lives in x-data and the show and hide animations are handled by x-transition.
The toggle button and its icons:
@click="open = !open": toggles the chat bubble from the floating button.x-show="!open"andx-show="open": decide which of the two icons is visible.absolute: positions the icons on top of each other so they can swap in place.x-transition:enterandx-transition:leavewith their start and end values: rotate, scale and fade the icons as they swap.
Classes and irrelevant content are removed for brevity, but I’ll keep those classes relevant to the tutorial.
<!-- toggle buttons -->
<button
@click="open = !open">
<svg
class="absolute"
x-show="!open"
x-transition:enter-start="opacity-0 -rotate-45 scale-75"
x-transition:enter="transition duration-200 transform ease"
x-transition:leave="transition duration-100 transform ease"
x-transition:leave-end="opacity-0 -rotate-45">
<!-- SVG Content goes here -->
</svg>
<svg
class="absolute"
x-show="open"
x-transition:enter-start="opacity-0 rotate-45 scale-75"
x-transition:enter="transition duration-200 transform ease"
x-transition:leave="transition duration-100 transform ease"
x-transition:leave-end="opacity-0 rotate-45">
<!-- SVG Content goes here -->
</svg>
</button>
</div>The chat bubble itself:
x-data="{ open: false }": the data object that holds the open state.x-show="open": shows the panel while it’s open.x-transition:enter-start="opacity-0 translate-y-5"andx-transition:leave-end="opacity-0 translate-y-5": fade and slide the panel in and out over 200ms.@click.away="open = false": closes the chat bubble when the user clicks outside of it.- The rest of the classes position and style the panel.
Classes and irrelevant content are removed for brevity, but I’ll keep those classes relevant to the tutorial.
<div x-data="{ open: false }">
<div
id="#"
x-show="open"
x-transition:enter-start="opacity-0 translate-y-5"
x-transition:enter="transition duration-200 transform ease"
x-transition:leave="transition duration-200 transform ease"
x-transition:leave-end="opacity-0 translate-y-5"
@click.away="open = false"
class="fixed flex flex-col z-50 bottom-[100px] top-0 ring-0 left-0 sm:top-auto sm:right-5 sm:left-auto h-[calc(100%-95px)] w-full sm:w-[350px] overflow-auto min-h-[250px] sm:h-[600px] "
>
<!-- Chat Bubble goes here -->
</div>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
If Alpine is already on the page, x-show plus x-transition handles the icon swap and the panel animation without any timeouts. If not, the vanilla version gets you the same effect with a couple of class toggles.
Conclusion
This is a simple chat bubble and a great starting point for building more complex ones. Remember to add validation and error handling to keep it accurate, secure and accessible. Something good to know: there are already companies that offer chat bubbles, so you can use one of those and avoid all the hassle of building your own.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza