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.
  • hidden and flex-col: the visibility toggle; the script swaps hidden for flex when 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. The bottom-[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" and id="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.

html
<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 grabs modal, toggleButton, openIcon and closeIcon.
  • toggleButton.addEventListener("click", ...): checks modal.classList.contains("hidden") to know the current state.
  • Opening: removes hidden, adds flex, then a 10ms setTimeout removes opacity-0 and translate-y-5 so the transition can play; the icons swap.
  • Closing: adds opacity-0 and translate-y-5 first, then a 200ms setTimeout, matching the transition duration, adds hidden back and removes flex; the icons swap back.
  • The document click listener closes the panel the same way when the click lands outside both the modal and the toggle button.
js
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" and x-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:enter and x-transition:leave with 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.

html
  <!-- 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" and x-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.

html
<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