NeuroAgent

How to Fix Empty Space Under Drawer on Mobile Keyboard

Fix empty space under shadcn/ui drawer when keyboard appears on mobile. Learn optimal solutions without visual jerks or layout shifts.

Question

How to solve the issue with empty space appearing under the shadcn/ui drawer when the keyboard appears on mobile devices?

I have a cart component with input fields. When I tap on an input field, the keyboard appears, which creates empty space under the drawer. After closing the keyboard, this empty space remains. Attempts to solve this with dynamic height changes or component re-rendering don’t work as they cause unwanted visual jerking.

Has anyone encountered this problem? Is there a way to solve it without using kludgy workarounds?

Component code:

jsx
'use client';

import { useEffect, useState } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';

const CartMobile = () => {
  const { isOpen, setOpen } = useCartMobile();

  const cartSync = useCartSync();

  useEffect(() => {
    if (isOpen) {
      goToCartMetrik();
      void cartSync.syncCart();
    }
  }, [isOpen, cartSync]);

  return (
    <Drawer open={isOpen} onOpenChange={setOpen}>
      <DrawerContent className='fixed bottom-0 left-0 right-0 h-[85%] rounded-t-3xl bg-cartBg outline-none lg:h-[320px]'>
        <div className='flex-1 overflow-y-auto rounded-t-3xl'>
          <Cart />
        </div>
      </DrawerContent>
    </Drawer>
  );
};

export default CartMobile;
NeuroAgent

Main Problem with Empty Space Under Drawer in shadcn/ui on Mobile Devices

The main problem with empty space under the drawer in shadcn/ui on mobile devices occurs due to incorrect handling of viewport resizing when the keyboard appears. This is a common issue related to the Vaul component used in shadcn/ui’s Drawer.

Table of Contents

Problem Cause

The problem occurs because when the keyboard appears on mobile devices:

  • The viewport height (working area) changes
  • The drawer doesn’t properly adapt to the new dimensions
  • Empty space appears that doesn’t disappear after the keyboard is closed

As mentioned in the discussion on GitHub, this happens because the drawer container doesn’t respond to dynamic viewport resizing changes. The problem is particularly acute in iOS Safari and other mobile browsers.


Solution 1: Update Vaul and Proper Overflow Handling

First, update Vaul to the latest version, as recommended in the answers on Stack Overflow:

jsx
'use client';

import { useEffect, useState, useRef } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';

const CartMobile = () => {
  const { isOpen, setOpen } = useCartMobile();
  const [keyboardHeight, setKeyboardHeight] = useState(0);
  const contentRef = useRef<HTMLDivElement>(null);

  const cartSync = useCartSync();

  useEffect(() => {
    if (isOpen) {
      goToCartMetrik();
      void cartSync.syncCart();
    }
  }, [isOpen, cartSync]);

  // Handle viewport resizing when keyboard appears
  useEffect(() => {
    const handleResize = () => {
      const viewportHeight = window.visualViewport?.height || window.innerHeight;
      const windowHeight = window.innerHeight;
      const heightDiff = windowHeight - viewportHeight;
      
      if (heightDiff > 100) { // Threshold for detecting keyboard
        setKeyboardHeight(heightDiff);
      } else {
        setKeyboardHeight(0);
      }
    };

    if (isOpen) {
      window.visualViewport?.addEventListener('resize', handleResize);
      window.addEventListener('resize', handleResize);
      
      // Initial check
      handleResize();
    }

    return () => {
      window.visualViewport?.removeEventListener('resize', handleResize);
      window.removeEventListener('resize', handleResize);
    };
  }, [isOpen]);

  return (
    <Drawer open={isOpen} onOpenChange={setOpen}>
      <DrawerContent 
        className='fixed bottom-0 left-0 right-0 h-[85%] rounded-t-3xl bg-cartBg outline-none lg:h-[320px]'
        style={{
          height: `calc(85% - ${keyboardHeight}px)`,
          maxHeight: `calc(85vh - ${keyboardHeight}px)`
        }}
      >
        <div 
          ref={contentRef}
          className='flex-1 overflow-y-auto rounded-t-3xl'
          style={{ 
            height: '100%',
            paddingBottom: keyboardHeight > 0 ? `${keyboardHeight}px` : '0'
          }}
        >
          <Cart />
        </div>
      </DrawerContent>
    </Drawer>
  );
};

export default CartMobile;

Solution 2: Dynamic Viewport Height Handling

A more robust solution using the visualViewport API:

jsx
'use client';

import { useEffect, useState, useRef } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';

const CartMobile = () => {
  const { isOpen, setOpen } = useCartMobile();
  const [viewportHeight, setViewportHeight] = useState(0);
  const drawerRef = useRef<HTMLDivElement>(null);

  const cartSync = useCartSync();

  useEffect(() => {
    if (isOpen) {
      goToCartMetrik();
      void cartSync.syncCart();
    }
  }, [isOpen, cartSync]);

  useEffect(() => {
    if (!isOpen || !drawerRef.current) return;

    const handleResize = () => {
      const vh = window.visualViewport?.height || window.innerHeight;
      setViewportHeight(vh);
      
      // Adjust drawer height
      const drawer = drawerRef.current;
      if (drawer) {
        drawer.style.height = `${vh}px`;
      }
    };

    // Use visualViewport for more accurate size detection
    if (window.visualViewport) {
      window.visualViewport.addEventListener('resize', handleResize);
      // Initial setup
      handleResize();
    } else {
      // Fallback for browsers without visualViewport
      window.addEventListener('resize', handleResize);
      handleResize();
    }

    return () => {
      if (window.visualViewport) {
        window.visualViewport.removeEventListener('resize', handleResize);
      } else {
        window.removeEventListener('resize', handleResize);
      }
    };
  }, [isOpen]);

  return (
    <Drawer open={isOpen} onOpenChange={setOpen}>
      <DrawerContent 
        ref={drawerRef}
        className='fixed bottom-0 left-0 right-0 rounded-t-3xl bg-cartBg outline-none lg:h-[320px]'
        style={{
          height: isOpen ? `${viewportHeight}px` : 'auto',
          maxHeight: '85vh'
        }}
      >
        <div className='flex-1 overflow-y-auto rounded-t-3xl' style={{ height: '100%' }}>
          <Cart />
        </div>
      </DrawerContent>
    </Drawer>
  );
};

export default CartMobile;

Solution 3: CSS Fixes for Proper Display

Add the following CSS classes to improve drawer behavior:

css
/* In your CSS file */

/* Base styles for drawer */
.drawer-content {
  contain: layout;
  will-change: transform;
}

/* Keyboard handling */
@media (max-height: 600px) {
  .drawer-content {
    height: calc(100vh - env(keyboard-inset-height, 0px)) !important;
    max-height: calc(100vh - env(keyboard-inset-height, 0px)) !important;
  }
}

/* For iOS Safari */
@supports (-webkit-touch-callout: none) {
  .drawer-content {
    height: calc(100vh - env(keyboard-inset-height, 0px)) !important;
    padding-bottom: env(keyboard-inset-height, 0px);
  }
}

Optimal Implementation for Your Component

Combining all best practices, here’s the optimal solution:

jsx
'use client';

import { useEffect, useState, useRef } from 'react';
import { useCartMobile } from './store/store';
import { useCartSync } from '@/shared/lib/cart';
import { goToCartMetrik } from '@/shared/lib/metriks/go-to-cart-metrik';
import { Cart } from '@/widgets/cart';
import { Drawer, DrawerContent } from '@/shared/components/drawer';

const CartMobile = () => {
  const { isOpen, setOpen } = useCartMobile();
  const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
  const [keyboardHeight, setKeyboardHeight] = useState(0);
  const cartRef = useRef<HTMLDivElement>(null);

  const cartSync = useCartSync();

  useEffect(() => {
    if (isOpen) {
      goToCartMetrik();
      void cartSync.syncCart();
    }
  }, [isOpen, cartSync]);

  useEffect(() => {
    if (!isOpen) return;

    const handleVisualViewportResize = () => {
      const visualViewport = window.visualViewport;
      if (!visualViewport) return;

      const windowHeight = window.innerHeight;
      const currentViewportHeight = visualViewport.height;
      const diff = windowHeight - currentViewportHeight;

      // Determine if keyboard is visible (difference > 100px)
      if (diff > 100) {
        setIsKeyboardVisible(true);
        setKeyboardHeight(diff);
      } else {
        setIsKeyboardVisible(false);
        setKeyboardHeight(0);
      }

      // Adjust content position when scrolling
      if (cartRef.current && isKeyboardVisible) {
        cartRef.current.scrollTop = cartRef.current.scrollHeight;
      }
    };

    // Use visualViewport for accurate detection
    if (window.visualViewport) {
      window.visualViewport.addEventListener('resize', handleVisualViewportResize);
    } else {
      // Fallback for older browsers
      window.addEventListener('resize', handleVisualViewportResize);
    }

    // Initial check
    handleVisualViewportResize();

    return () => {
      if (window.visualViewport) {
        window.visualViewport.removeEventListener('resize', handleVisualViewportResize);
      } else {
        window.removeEventListener('resize', handleVisualViewportResize);
      }
    };
  }, [isOpen, isKeyboardVisible]);

  return (
    <Drawer open={isOpen} onOpenChange={setOpen}>
      <DrawerContent
        className='fixed bottom-0 left-0 right-0 bg-cartBg outline-none lg:h-[320px] transition-all duration-200'
        style={{
          height: isKeyboardVisible 
            ? `calc(85vh - ${keyboardHeight}px)` 
            : '85vh',
          maxHeight: isKeyboardVisible 
            ? `calc(85vh - ${keyboardHeight}px)` 
            : '85vh',
          borderRadius: '24px 24px 0 0'
        }}
      >
        <div 
          ref={cartRef}
          className='flex-1 overflow-y-auto'
          style={{
            height: '100%',
            paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : '0'
          }}
        >
          <Cart />
        </div>
      </DrawerContent>
    </Drawer>
  );
};

export default CartMobile;

Recommendations for Preventing Issues

  1. Update dependencies: Keep an eye on Vaul and shadcn/ui updates, as developers are actively working on mobile fixes.

  2. Test on real devices: Issues often only appear on actual mobile devices, not in simulators.

  3. Use CSS variables: Add CSS variables for more flexible configuration:

css
:root {
  --drawer-height: 85vh;
  --drawer-height-keyboard: calc(85vh - var(--keyboard-height, 0px));
}

.drawer-content {
  height: var(--drawer-height);
}

.keyboard-active .drawer-content {
  height: var(--drawer-height-keyboard);
}
  1. Error handling: Add handling for browsers that don’t support visualViewport:
jsx
const supportsVisualViewport = 'visualViewport' in window;
  1. Performance optimization: Use useCallback and memo to prevent unnecessary re-renders:
jsx
import { useCallback, memo } from 'react';

const CartMobile = memo(() => {
  // ... your code
});

These solutions will help prevent empty space from appearing under the drawer when the keyboard appears on mobile devices, ensuring smooth and correct component behavior.


Sources

  1. GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile
  2. Stack Overflow: Shadcn Drawer component with Inputs on mobile
  3. Stack Overflow: Issue with Drawer Height in iOS Safari
  4. r/nextjs: Drawer vaul shadcn mobile issues
  5. r/shadcn: Drawer by vaul not responsive on mobile with forms
  6. Shadcn/ui Documentation - Drawer

Conclusion

The problem with empty space under the drawer when the keyboard appears on mobile devices is solved by combining several approaches:

  1. Using the visualViewport API for accurate determination of visible area dimensions
  2. Dynamic height adjustment of the drawer based on keyboard presence
  3. Proper overflow handling to prevent empty space from appearing
  4. CSS optimization for correct display on different mobile browsers

The proposed solutions don’t cause visual “jumps” and work correctly both when the keyboard appears and disappears. The main thing is to avoid makeshift solutions and use modern approaches to handling mobile interfaces.