Tutorials / / 4 min read
How to create a bottom drawer with Tailwind CSS: JavaScript and Alpine.js
Build a bottom drawer that slides up from the bottom of the screen with Tailwind CSS, two ways: vanilla JavaScript class toggling or Alpine.js transitions.
Today we are building a bottom drawer: a panel that slides up from the bottom of the screen. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a bottom drawer?
A bottom drawer is a panel anchored to the bottom of the screen that slides up to present extra information or options without leaving the page: think cookie settings, a navigation menu, recent items or a settings panel.
Use cases
- Navigation menus: easy access to sections on mobile without occupying much screen space.
- Settings panels: a consistent spot for preferences users adjust often, like cookie choices.
- Modals and prompts: present dialogs or contextual messages without pulling users out of their task.
- Tooltips and help: surface supplementary details without obstructing the main content.
The markup
id="toggleButton"witharia-expanded="false": the trigger that toggles the bottom drawer.id="bottomDrawer": the container that holds the drawer.fixed inset-x-0 bottom-0: pins the drawer to the bottom edge of the screen, full width.transform translate-y-full: starts the drawer off-screen, pushed down by its own height.transition-transform duration-300: animates the slide over 300ms.id="rejectButton": the button that closes the drawer.
Irrelevant classes have been removed for the sake of brevity
<button id="toggleButton">Toggle Cookie Settings</button>
<div
id="bottomDrawer"
class="fixed inset-x-0 bottom-0 transform translate-y-full transition-transform duration-300"
>
<div>
<!-- Drawer content -->
<div class="flex items-center flex-none gap-x-5">
<button type="button">Accept all</button>
<button type="button" id="rejectButton">Reject all</button>
</div>
</div>
</div>The script
document.addEventListener("DOMContentLoaded", () => {: waits until the page is loaded, then grabstoggleButton,bottomDrawerandrejectButton.toggleDrawer(): togglestranslate-y-fullandtranslate-y-0to slide the drawer in and out.closeDrawer(): addstranslate-y-fulland removestranslate-y-0so the drawer always ends up closed.toggleButton.addEventListener("click", toggleDrawer);: toggles the drawer when the trigger is clicked.rejectButton.addEventListener("click", closeDrawer);: closes the drawer from the reject button.- The
documentclick listener checks!bottomDrawer.contains(event.target)and!toggleButton.contains(event.target)to close the drawer when the user clicks outside of it.
document.addEventListener("DOMContentLoaded", () => {
const toggleButton = document.getElementById("toggleButton");
const bottomDrawer = document.getElementById("bottomDrawer");
const rejectButton = document.getElementById("rejectButton");
function toggleDrawer() {
bottomDrawer.classList.toggle("translate-y-full");
bottomDrawer.classList.toggle("translate-y-0");
}
function closeDrawer() {
bottomDrawer.classList.add("translate-y-full");
bottomDrawer.classList.remove("translate-y-0");
}
toggleButton.addEventListener("click", toggleDrawer);
rejectButton.addEventListener("click", closeDrawer);
document.addEventListener("click", (event) => {
if (
!bottomDrawer.contains(event.target) &&
!toggleButton.contains(event.target)
) {
closeDrawer();
}
});
});The Alpine.js version
Same component, no separate script: the state lives in x-data and the slide is handled by x-transition.
x-data="{ isOpen: false }": stores the state of the bottom drawer.@click="isOpen = !isOpen": toggles it from the trigger button.x-show="isOpen": shows the container while the drawer is open.@click.away="isOpen = false": closes the drawer when the user clicks outside of it.x-transition:enter="transition ease-out duration-300"with itsenter-startandenter-endvalues: slides and fades the drawer in.x-transition:leave="transition ease-in duration-300"with itsleave-startandleave-endvalues: slides and fades it back out.@click="isOpen = false"on the reject button: closes the drawer.
<!-- Button to toggle the bottom drawer -->
<button @click="isOpen = !isOpen" aria-expanded="false">
Toggle Cookie Settings
</button>
<!-- Bottom drawer container with transition -->
<div x-show="isOpen" @click.away="isOpen = false">
<div
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="transform opacity-0 translate-y-full"
x-transition:enter-end="transform opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="transform opacity-100 translate-y-0"
x-transition:leave-end="transform opacity-0 translate-y-full"
>
<!-- Drawer content -->
<p>
<!-- Text goes here -->
<span> See our <a href="#">cookie policy</a>. </span>
</p>
<div>
<button type="button">Accept all</button>
<button type="button" @click="isOpen = false">Reject all</button>
</div>
</div>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
If Alpine is already loaded, x-show with x-transition gives you the slide and the click-away behavior with zero script. If not, the vanilla version needs just a few lines and no dependency.
Conclusion
This is a simple bottom drawer that can be used for any type of content, such as a cookie policy, navigation menu, or settings panel, you name it. Remember to make it as accessible as possible.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza