How to build a full-screen navigation with Tailwind CSS and JavaScript
Learn how to create a smooth, full-screen overlay navigation menu using Tailwind CSS for styling and vanilla JavaScript for state management.
Published on December 6, 2025 by Michael AndreuzzaFull-screen navigation menus are excellent for creating immersive experiences and handling complex site structures without cluttering the header.
In this tutorial, we’ll build a responsive full-screen menu that slides down from the top, locking the body scroll to prevent background movement.
Why Use a Full-Screen Navigation?
Full-screen navigations are popular in modern web design for several reasons:
- Visual Impact: They create a bold, immersive experience that draws attention to your navigation items.
- Mobile-First Thinking: The pattern works seamlessly across all screen sizes without needing separate mobile/desktop implementations.
- Content Flexibility: You have the entire viewport to display navigation links, images, contact information, or even promotional content.
- Focus: When open, users can focus entirely on navigation without distractions from the page content.
This pattern is commonly used by agencies, portfolios, and brands that want to make a strong visual statement.
The Structure
We use a fixed header for the toggle button and a fixed overlay div for the menu itself. The overlay is hidden by default using a negative translate transform.
Breaking Down the HTML
The structure consists of two main parts:
- The Navigation Bar: A fixed header containing the logo, optional contact info, and the menu toggle button.
- The Full-Screen Overlay: A fixed
divthat covers the entire viewport when opened.
Key Tailwind Classes for the Overlay
fixed inset-0 bg-white z-[60] transition-transform duration-500 ease-in-out flex flex-col overflow-hidden -translate-y-full
Let’s break these down:
fixed inset-0: Positions the overlay to cover the entire viewport.z-[60]: Ensures the overlay sits above the navigation bar (which usesz-50).transition-transform duration-500 ease-in-out: Creates a smooth 500ms animation when the menu opens/closes.-translate-y-full: Moves the overlay completely off-screen (above the viewport) by default.overflow-hidden: Prevents any content from spilling outside the overlay.
When we remove -translate-y-full, the overlay animates smoothly into view thanks to the transition-transform property.
The Logic
We need a small script to handle the toggling. The key here is handling the overflow-hidden on the body to prevent scrolling when the menu is open, and calculating the scrollbar width to prevent layout shifts.
The Toggle Function Explained
function toggleMenu() {
if (!fullscreenMenu) return;
if (fullscreenMenu.classList.contains("-translate-y-full")) {
// Open Menu
const scrollbarWidth =
window.innerWidth - document.documentElement.clientWidth;
document.body.style.paddingRight = `${scrollbarWidth}px`;
document.body.classList.add("overflow-hidden");
fullscreenMenu.classList.remove("-translate-y-full");
} else {
// Close Menu
document.body.classList.remove("overflow-hidden");
document.body.style.paddingRight = "";
fullscreenMenu.classList.add("-translate-y-full");
}
}
What’s Happening Here?
- State Check: We check if the menu is currently hidden by looking for the
-translate-y-fullclass. - Scrollbar Width Calculation: Before opening, we calculate the scrollbar width to prevent layout shift.
- Body Padding Compensation: We add padding equal to the scrollbar width so the content doesn’t jump.
- Scroll Lock: We add
overflow-hiddento the body to prevent background scrolling. - Toggle Animation: We remove/add the translate class to trigger the CSS transition.
Key Implementation Details
1. Transition State
We use -translate-y-full to hide the menu off-screen (top). Removing this class lets the transition-transform property animate it into view (translate-y-0).
This approach is more performant than animating top or height properties because transforms are GPU-accelerated and don’t trigger layout recalculations.
2. Scroll Locking
When the menu opens, we add overflow-hidden to the body. This prevents users from scrolling the background content while the menu is open.
Without this, users could scroll the page behind the overlay, which creates a confusing experience.
3. Layout Shift Prevention
This is the most commonly overlooked detail. When we hide the scrollbar with overflow-hidden, the page content shifts slightly to the right (by the width of the scrollbar).
We solve this by:
- Calculating the scrollbar width:
window.innerWidth - document.documentElement.clientWidth - Adding that value as
padding-rightto the body before hiding the scrollbar
This ensures the content stays perfectly still when the menu opens and closes.
Accessibility Considerations
When building full-screen navigations, keep these accessibility best practices in mind:
Keyboard Navigation
- Ensure all menu links are focusable and can be navigated with the Tab key.
- Add keyboard support to close the menu with the Escape key.
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
!fullscreenMenu.classList.contains("-translate-y-full")
) {
toggleMenu();
}
});
ARIA Attributes
Consider adding ARIA attributes for better screen reader support:
<button
id="menu-toggle"
aria-expanded="false"
aria-controls="fullscreen-menu"
aria-label="Toggle Menu"
></button>
Update aria-expanded in your JavaScript when toggling:
menuToggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
Focus Management
When the menu opens, consider moving focus to the first menu item or the close button. When it closes, return focus to the toggle button.
Customization Ideas
Different Animation Directions
Instead of sliding from the top, you can slide from other directions:
- From bottom: Use
translate-y-fullinstead of-translate-y-full - From left: Use
-translate-x-full - From right: Use
translate-x-full
Fade Effect
Add opacity to create a fade-in effect:
opacity-0 transition-all duration-500
Then toggle opacity-100 along with the transform.
Staggered Link Animations
For a more polished feel, animate each link with a slight delay:
.menu-link {
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
}
.menu-open .menu-link {
opacity: 1;
transform: translateY(0);
}
.menu-link:nth-child(1) {
transition-delay: 0.1s;
}
.menu-link:nth-child(2) {
transition-delay: 0.15s;
}
.menu-link:nth-child(3) {
transition-delay: 0.2s;
}
/* ... and so on */
The Full Code
Here is the complete code separated into HTML structure and JavaScript logic.
HTML Structure
<nav class="fixed w-full top-0 z-50 bg-white">
<div class="max-w-screen mx-auto px-8 w-full 2xl:max-w-[100rem]">
<div class="flex py-2 items-start lg:items-center justify-between">
<!-- Logo -->
<a href="/" aria-label="Home" class="focus:outline-none">
<svg
class="h-8 text-base-900"
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 0V80H80V0H0ZM8.68501 35.7187H71.315V8.68502H8.68501V35.7187ZM71.315 71.315H8.68501V44.4037H71.315V71.315Z"
fill="currentColor"
></path>
</svg>
</a>
<!-- Contact Info (Optional) -->
<div class="flex flex-col text-center">
<a
href="mailto:hello@rosewood.com"
class="text-[0.65rem] text-base-600 font-medium uppercase tracking-wide hover:text-bse-500 transition-colors"
>
hello@rosewood.com
</a>
<a
href="tel:+15551234567"
class="text-[0.65rem] text-base-600 font-medium uppercase tracking-wide hover:text-bse-500 transition-colors"
>
+1 (555) 123-4567
</a>
</div>
<!-- Menu Toggle Button -->
<button
id="menu-toggle"
class="focus:outline-none"
aria-label="Toggle Menu"
>
<span class="sr-only">Menu</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
class="icon size-6 group-hover:text-bse-500 transition-colors"
>
<path
d="M228,160a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,160ZM40,108H216a12,12,0,0,0,0-24H40a12,12,0,0,0,0,24Z"
></path>
</svg>
</button>
</div>
</div>
<!-- Full Screen Overlay -->
<div
id="fullscreen-menu"
class="fixed inset-0 bg-white z-[60] transition-transform duration-500 ease-in-out flex flex-col overflow-hidden -translate-y-full"
>
<div
class="max-w-screen mx-auto px-8 w-full 2xl:max-w-[100rem] py-2 h-full flex flex-col"
>
<!-- Header inside overlay (Logo + Close Button) -->
<div class="flex items-center justify-between">
<a href="/" aria-label="Home" class="focus:outline-none">
<svg
class="h-8 text-base-900"
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 0V80H80V0H0ZM8.68501 35.7187H71.315V8.68502H8.68501V35.7187ZM71.315 71.315H8.68501V44.4037H71.315V71.315Z"
fill="currentColor"
></path>
</svg>
</a>
<button id="menu-close" class="focus:outline-none">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
class="size-6 text-base-900 hover:text-bse-500 transition-colors"
>
<path
d="M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"
></path>
</svg>
</button>
</div>
<!-- Menu Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 items-start h-full mt-2">
<h1
class="text-5xl sm:text-5xl md:text-6xl lg:text-7xl text-base-900 leading-none font-display"
>
Menu
</h1>
<div class="flex flex-col gap-2 mt-auto lg:mt-0">
<a
href="/system/overview"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Overview</a
>
<a
href="https://buy.polar.sh/polar_cl_R1DzAX5Wlvr5Qnqx8VysnG7tkliXHnU4qs9bQ0AaQMb"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Buy Rosewood</a
>
<a
href="/services"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Services</a
>
<a
href="/projects"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Projects</a
>
<a
href="/about"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>About</a
>
<a
href="/process"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Process</a
>
<a
href="/blog"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Blog</a
>
<a
href="/contact"
class="w-fit text-base-900 text-base block hover:text-base-500 duration-300"
>Contact</a
>
</div>
<img
src="/_astro/hero.DQ0YEZB6_ZIJDlc.webp"
alt="Rosewood Home Improvement"
loading="lazy"
decoding="async"
fetchpriority="auto"
width="1200"
height="1200"
class="size-full object-cover hidden lg:block"
/>
</div>
</div>
</div>
</nav>
JavaScript
const menuToggle = document.getElementById("menu-toggle");
const menuClose = document.getElementById("menu-close");
const fullscreenMenu = document.getElementById("fullscreen-menu");
function toggleMenu() {
if (!fullscreenMenu) return;
if (fullscreenMenu.classList.contains("-translate-y-full")) {
// Open Menu
const scrollbarWidth =
window.innerWidth - document.documentElement.clientWidth;
document.body.style.paddingRight = `${scrollbarWidth}px`;
document.body.classList.add("overflow-hidden");
fullscreenMenu.classList.remove("-translate-y-full");
} else {
// Close Menu
document.body.classList.remove("overflow-hidden");
document.body.style.paddingRight = "";
fullscreenMenu.classList.add("-translate-y-full");
}
}
menuToggle?.addEventListener("click", toggleMenu);
menuClose?.addEventListener("click", toggleMenu);
// Close on Escape key
document.addEventListener("keydown", (e) => {
if (
e.key === "Escape" &&
!fullscreenMenu?.classList.contains("-translate-y-full")
) {
toggleMenu();
}
});
Conclusion
This approach ensures a smooth, professional feel that works across devices. The key takeaways are:
- Use CSS transforms for performant animations.
- Always handle scroll locking to prevent background scrolling.
- Compensate for scrollbar width to avoid layout shifts.
- Consider accessibility with keyboard navigation and ARIA attributes.
With these foundations in place, you can customize the design to match your brand while maintaining a solid, accessible user experience.
/Michael Andreuzza