GLM 4.5 Air

Next.js Full Viewport Sections with Tailwind CSS Guide

Learn how to create full viewport sections in Next.js with Tailwind CSS that properly handle mobile status bars and navigation bars. Complete guide with code examples and solutions for mobile viewport issues.

Question

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?

GLM 4.5 Air

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

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.
css
/* 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

css
/* 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:

jsx
<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:

javascript
// 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:

jsx
<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:

css
/* 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:

javascript
// 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:

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

jsx
// 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

jsx
// 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

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

jsx
// 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

jsx
// 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:

jsx
// 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

jsx
// 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:

jsx
// 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:

  1. Use 100dvh instead of 100vh to handle dynamic viewport changes on mobile devices.

  2. Implement safe area handling using CSS environment variables for devices with notches or navigation bars.

  3. Structure your layout with flexbox to ensure proper section expansion and content flow.

  4. Test thoroughly on real devices and across different browsers to catch viewport-specific issues.

  5. 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.