How do I create full viewport sections in Next.js with Tailwind CSS that properly override the status bar and navigation bar on mobile devices? I’m implementing a layout with full-height sections using 100dvh and 100vh units, but on mobile devices, the sections don’t extend to cover the entire screen - there are gaps at the top (status bar area) and bottom (navigation bar area). I’ve tried using CSS variables for safe area insets and viewport units, but the sections still don’t take up the full viewport height on mobile. What’s the best approach to ensure sections cover the entire viewport while properly handling mobile device safe areas, status bars, and navigation bars in a Next.js application with Tailwind CSS?
Creating Full Viewport Sections in Next.js with Tailwind CSS
The best approach to create full viewport sections in Next.js with Tailwind CSS that properly handle mobile device status bars and navigation bars is using 100dvh
(dynamic viewport height) units combined with custom CSS variables for safe area insets. This ensures your sections adapt to dynamic UI elements like keyboard appearances while covering the entire screen on all devices.
Contents
- Understanding Viewport Units in Mobile Browsers
- The Difference Between vh and dvh Units
- Implementing Full Viewport Sections with Tailwind
- Handling Safe Areas with CSS Variables
- Next.js Specific Implementation
- Advanced Techniques for Dynamic Navigation Bars
- Testing and Debugging Viewport Issues
Understanding Viewport Units in Mobile Browsers
Mobile browsers present unique challenges for viewport-based layouts due to dynamic UI elements like status bars, navigation bars, and virtual keyboards. Unlike desktop browsers, mobile browsers’ viewport height can change dynamically when these UI elements appear or disappear.
Dynamic Viewport Behavior: When a mobile browser’s status bar or navigation bar changes (e.g., when scrolling or when the keyboard appears), the viewport height changes. This causes traditional
100vh
units to miscalculate the available space, creating gaps or overflow issues.
The key insight is that mobile browsers don’t provide a stable viewport height throughout the user experience, which is why implementing proper full-viewport sections requires special considerations.
The Difference Between vh and dvh Units
Understanding the distinction between viewport height units is crucial for solving your issue:
100vh
- Viewport Height: Represents 1% of the initial viewport height. This value is fixed after the page loads and doesn’t adjust when browser UI elements change.100dvh
- Dynamic Viewport Height: Represents 1% of the current viewport height, which adjusts dynamically when browser UI elements like the keyboard or navigation bars appear/disappear.
/* Traditional approach (problematic on mobile) */
.full-viewport {
height: 100vh; /* Fixed after initial render */
}
/* Improved approach (handles dynamic UI elements) */
.full-viewport {
height: 100dvh; /* Adjusts dynamically */
}
For modern mobile-first applications, 100dvh
is almost always preferable over 100vh
as it adapts to the changing viewport conditions.
Implementing Full Viewport Sections with Tailwind
Here’s how to implement full viewport sections in Tailwind CSS:
Basic Implementation
/* In your global CSS or Tailwind config */
@layer utilities {
.min-h-screen-dvh {
min-height: 100dvh;
}
.h-screen-dvh {
height: 100dvh;
}
}
Then use these classes in your components:
<section className="h-screen-dvh flex items-center justify-center">
{/* Your content */}
</section>
Tailwind CSS Configuration Approach
Alternatively, extend your Tailwind config to include these utilities:
// tailwind.config.js
module.exports = {
theme: {
extend: {
height: {
'dvh': '100dvh',
'dvh-screen': '100dvh',
},
minHeight: {
'dvh': '100dvh',
'dvh-screen': '100dvh',
}
}
}
}
This allows you to use classes like h-dvh
or min-h-dvh
directly in your components:
<section className="h-dvh flex flex-col">
<div className="flex-grow">
{/* Expandable content */}
</div>
<footer className="py-4">
{/* Fixed height footer */}
</footer>
</section>
Handling Safe Areas with CSS Variables
To properly account for device-specific safe areas (like the iPhone notch or Android navigation bar), you can use CSS environment variables:
/* Global CSS */
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
--safe-area-inset-left: env(safe-area-inset-left, 0);
--safe-area-inset-right: env(safe-area-inset-right, 0);
}
.full-viewport-with-safe-area {
height: calc(100dvh - var(--safe-area-inset-top) - var(--safe-area-inset-bottom));
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
}
In Tailwind CSS, you can create utilities for these:
// tailwind.config.js
module.exports = {
theme: {
extend: {
inset: {
'safe-top': 'env(safe-area-inset-top, 0)',
'safe-bottom': 'env(safe-area-inset-bottom, 0)',
'safe-left': 'env(safe-area-inset-left, 0)',
'safe-right': 'env(safe-area-inset-right, 0)',
}
}
}
}
Then use them like:
<section className="h-dvh pt-safe-top pb-safe-bottom">
{/* Content */}
</section>
Next.js Specific Implementation
In a Next.js application, you’ll typically want to implement these viewport solutions across your layout. Here’s a complete approach:
1. Create a Layout Component
// components/Layout.js
import React from 'react';
export default function Layout({ children }) {
return (
<div className="flex flex-col min-h-dvh">
<header className="h-16 md:h-20 flex-shrink-0">
{/* Your navigation bar */}
</header>
<main className="flex-grow overflow-y-auto">
{children}
</main>
<footer className="h-16 flex-shrink-0">
{/* Your footer */}
</footer>
</div>
);
}
2. Create Full Viewport Sections
// sections/HeroSection.js
import React from 'react';
export default function HeroSection() {
return (
<section className="h-dvh flex flex-col">
<div className="flex-grow flex items-center justify-center px-4">
<h1 className="text-4xl md:text-6xl font-bold text-center">
Hero Content
</h1>
</div>
<div className="h-16 flex-shrink-0 flex items-center justify-center">
{/* Scroll indicator or CTA */}
</div>
</section>
);
}
3. Add Global CSS
/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.min-h-screen-dvh {
min-height: 100dvh;
}
.h-screen-dvh {
height: 100dvh;
}
.min-h-screen-dvh-with-padding {
min-height: calc(100dvh - 4rem); /* Adjust based on your header/footer height */
}
}
/* Define safe area variables */
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
}
4. Use the Layout in Your Pages
// pages/index.js
import Layout from '../components/Layout';
import HeroSection from '../sections/HeroSection';
import AnotherSection from '../sections/AnotherSection';
export default function Home() {
return (
<Layout>
<HeroSection />
<AnotherSection />
{/* More sections */}
</Layout>
);
}
Advanced Techniques for Dynamic Navigation Bars
For applications with dynamic navigation bars that may appear/disappear (like when scrolling), you need more sophisticated solutions:
1. JavaScript-based Viewport Height Adjustment
// hooks/useViewportHeight.js
import { useState, useEffect } from 'react';
export default function useViewportHeight() {
const [height, setHeight] = useState(0);
useEffect(() => {
const setVH = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
setHeight(window.innerHeight);
};
setVH();
// Recalculate on resize and orientation change
window.addEventListener('resize', setVH);
window.addEventListener('orientationchange', setVH);
return () => {
window.removeEventListener('resize', setVH);
window.removeEventListener('orientationchange', setVH);
};
}, []);
return height;
}
Then use this custom hook in your components:
// components/DynamicViewportSection.js
import useViewportHeight from '../hooks/useViewportHeight';
export default function DynamicViewportSection({ children }) {
const vh = useViewportHeight();
return (
<section
className="relative"
style={{ height: `${vh}px` }}
>
{children}
</section>
);
}
2. Intersection Observer for Dynamic Navigation
// components/Navigation.js
import { useState, useEffect, useRef } from 'react';
export default function Navigation() {
const [isVisible, setIsVisible] = useState(true);
const lastScrollPosition = useRef(0);
const navRef = useRef(null);
useEffect(() => {
const handleScroll = () => {
const currentScrollPosition = window.pageYOffset;
// Hide/show based on scroll direction
if (currentScrollPosition > lastScrollPosition.current && currentScrollPosition > 100) {
setIsVisible(false);
} else {
setIsVisible(true);
}
lastScrollPosition.current = currentScrollPosition;
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<nav
ref={navRef}
className={`fixed top-0 left-0 right-0 z-50 transition-transform duration-300 ${
isVisible ? 'translate-y-0' : '-translate-y-full'
}`}
>
{/* Navigation content */}
</nav>
);
}
Testing and Debugging Viewport Issues
To ensure your full viewport sections work correctly across all devices:
1. Use Device Emulation in Browser DevTools
- Chrome DevTools: Toggle device toolbar (Ctrl+Shift+M or Cmd+Shift+M)
- Test with various device presets
- Enable “Mobile” emulation settings
- Test with different device orientations
2. Test on Real Devices
- Test on both iOS and Android devices
- Test with different OS versions
- Test with different browsers (Safari, Chrome, Firefox, etc.)
- Pay special attention to devices with:
- Notches (iPhone X and newer)
- Dynamic islands (iPhone 14 Pro and newer)
- Navigation gestures (Android devices without hardware buttons)
3. Check Viewport Dimensions
Add this component to see the current viewport dimensions:
// components/ViewportInfo.js
import { useState, useEffect } from 'react';
export default function ViewportInfo() {
const [dimensions, setDimensions] = useState({
width: 0,
height: 0,
dvh: 0,
vh: 0
});
useEffect(() => {
const updateDimensions = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
dvh: window.innerHeight,
vh: Math.round(document.documentElement.clientHeight)
});
};
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => {
window.removeEventListener('resize', updateDimensions);
};
}, []);
return (
<div className="fixed bottom-4 right-4 bg-black bg-opacity-75 text-white p-2 rounded text-xs z-50">
<div>Width: {dimensions.width}px</div>
<div>Height: {dimensions.height}px</div>
<div>dvh: {dimensions.dvh}px</div>
<div>vh: {dimensions.vh}px</div>
</div>
);
}
Conclusion
Creating full viewport sections in Next.js with Tailwind CSS requires understanding the dynamic nature of mobile viewports. Here are the key takeaways:
-
Use
100dvh
instead of100vh
to handle dynamic viewport changes on mobile devices. -
Implement safe area handling using CSS environment variables for devices with notches or navigation bars.
-
Structure your layout with flexbox to ensure proper section expansion and content flow.
-
Test thoroughly on real devices and across different browsers to catch viewport-specific issues.
-
Consider JavaScript solutions for complex cases where CSS alone isn’t sufficient, such as with dynamic navigation bars.
By implementing these techniques, you’ll create full viewport sections that properly adapt to all mobile devices and handle the challenges of status bars, navigation bars, and other dynamic UI elements.