NeuroAgent

How to remove empty space under drawer on mobile devices

Solution for empty space under shadcn/ui drawer on iOS Safari. Using window.visualViewport API to adjust height when keyboard appears.

Question

How do I solve the issue of empty space appearing under the shadcn/ui drawer component 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. Solutions involving dynamic height changes or component re-rendering don’t work as they cause unwanted UI jerking.

Has anyone encountered this problem before, and is there a way to solve it without using 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

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

The issue with empty space appearing under the drawer component from shadcn/ui on mobile devices is a known problem related to how iOS Safari and the virtual keyboard work. The main reason is that iOS Safari changes the visual viewport when the keyboard opens but doesn’t change the layout viewport, which leads to empty space appearing. To solve this problem, you need to use the window.visualViewport API to track viewport size changes and dynamically adjust the drawer’s position and height.

Contents

Main Problem and Its Causes

The problem you’re encountering is a classic case for mobile browsers, especially iOS Safari. When a user taps on an input field in your cart component:

  1. The virtual keyboard appears and covers part of the screen
  2. iOS Safari changes the visual viewport (what the user sees), but doesn’t change the layout viewport (what determines element sizes)
  3. The drawer component maintains its original height, which leads to empty space appearing under it
  4. After closing the keyboard, the empty space remains because the viewport dimensions aren’t automatically restored

As explained by Martijn Hols, iOS Safari doesn’t support interactive-widget: resizes-content and instead shifts the layout viewport when the keyboard opens.

API window.visualViewport as a Solution

The most effective solution is to use the window.visualViewport API, which provides information about the device’s current visible viewport, including its size and position. This API is specifically designed to work with virtual keyboards and dynamic viewport changes.

javascript
// Example of viewport tracking
const handleVisualViewportChange = () => {
  const visualViewportHeight = window.visualViewport.height;
  const keyboardHeight = window.innerHeight - visualViewportHeight;
  
  // You can log or use these values here
  console.log('Visual viewport height:', visualViewportHeight);
  console.log('Keyboard height:', keyboardHeight);
};

// Add event listener
window.visualViewport?.addEventListener('resize', handleVisualViewportChange);

// Remove listener when component unmounts
return () => {
  window.visualViewport?.removeEventListener('resize', handleVisualViewportChange);
};

As shown in the Stack Overflow answer, this approach allows you to accurately determine the keyboard height and adjust the drawer dimensions in real time.

Practical Solution for Your Component

For your cart component, I recommend the following solution:

jsx
'use client';

import { useEffect, useRef, 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();
  const drawerRef = useRef<HTMLDivElement>(null);
  const [keyboardHeight, setKeyboardHeight] = useState(0);

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

  useEffect(() => {
    // Function to handle viewport changes
    const handleVisualViewportChange = () => {
      const visualViewportHeight = window.visualViewport?.height || window.innerHeight;
      const currentKeyboardHeight = window.innerHeight - visualViewportHeight;
      
      setKeyboardHeight(currentKeyboardHeight);
      
      // If there's a drawerRef, adjust its height and position
      if (drawerRef.current) {
        const drawerElement = drawerRef.current;
        const drawerContent = drawerElement.querySelector('[data-drawer-content]');
        
        if (drawerContent) {
          // Adjust content height considering the keyboard
          const newHeight = Math.max(window.innerHeight - currentKeyboardHeight, 300);
          drawerContent.style.height = `${newHeight}px`;
          
          // If keyboard is open, add padding-bottom
          if (currentKeyboardHeight > 50) {
            drawerContent.style.paddingBottom = `${currentKeyboardHeight + 20}px`;
          } else {
            drawerContent.style.paddingBottom = '0';
          }
        }
      }
    };

    // Add event listeners only when drawer is open
    if (isOpen) {
      // Initialize on first open
      handleVisualViewportChange();
      
      // Add listeners to track changes
      window.visualViewport?.addEventListener('resize', handleVisualViewportChange);
      window.visualViewport?.addEventListener('scroll', handleVisualViewportChange);
    }

    // Clean up listeners
    return () => {
      window.visualViewport?.removeEventListener('resize', handleVisualViewportChange);
      window.visualViewport?.removeEventListener('scroll', handleVisualViewportChange);
    };
  }, [isOpen]);

  return (
    <Drawer open={isOpen} onOpenChange={setOpen}>
      <DrawerContent 
        ref={drawerRef}
        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'
          data-drawer-content
        >
          <Cart />
        </div>
      </DrawerContent>
    </Drawer>
  );
};

export default CartMobile;

Key points of this solution:

  1. Using ref for direct access to drawer elements
  2. Tracking window.visualViewport to determine keyboard height
  3. Dynamically changing height and padding of content
  4. Proper cleanup of event listeners when component unmounts

Alternative Approaches and Their Comparison

1. Updating Vaul to the Latest Version

As mentioned in the Stack Overflow, many users solved the problem by updating Vaul (the library underlying shadcn drawer) to the latest version:

bash
npm update @radix-ui/react-dialog
npm update @radix-ui/react-visually-hidden
npm update vaul

2. Adding CSS Class overflow-auto

The same source recommends adding className="overflow-auto" to your input field container:

jsx
<div className="overflow-auto">
  <Cart />
</div>

3. Working with CSS Viewport Units

Another approach is using CSS properties that consider dynamic viewport:

css
.drawer-content {
  min-height: 100vh;
  height: 100dvh;
  padding-bottom: env(safe-area-inset-bottom, 0);
}

Comparison of Approaches

Approach Advantages Disadvantages Recommendation
window.visualViewport Precise tracking, flexible configuration Requires additional code Most reliable solution
Updating Vaul Simplicity, minimal changes Doesn’t always solve the problem First thing to try
CSS viewport Minimal code May not work on iOS Supplement to main solution
overflow-auto Simplicity Doesn’t solve the root problem Recommended as supplement

Optimization for iOS Safari

iOS Safari has features that should be considered when solving this problem:

1. Support for env() Function

iOS Safari supports the env() function for accounting for system areas:

css
.drawer-content {
  padding-bottom: env(safe-area-inset-bottom, 0);
  height: calc(100dvh - env(safe-area-inset-bottom, 0));
}

2. Preventing Unwanted Scrolling

As noted by Saric Den, it’s important to prevent unwanted scrolling when the keyboard opens:

javascript
useEffect(() => {
  const preventScroll = (e: TouchEvent) => {
    if (isOpen && keyboardHeight > 50) {
      e.preventDefault();
    }
  };

  document.addEventListener('touchmove', preventScroll, { passive: false });
  
  return () => {
    document.removeEventListener('touchmove', preventScroll);
  };
}, [isOpen, keyboardHeight]);

3. Proper Keyboard Close Handling

Ensure that when the keyboard closes, all changes are reset:

javascript
const handleVisualViewportChange = () => {
  const visualViewportHeight = window.visualViewport?.height || window.innerHeight;
  const currentKeyboardHeight = window.innerHeight - visualViewportHeight;
  
  // If keyboard is closed, reset all changes
  if (currentKeyboardHeight < 50) {
    setKeyboardHeight(0);
    // You can add style reset here
  } else {
    setKeyboardHeight(currentKeyboardHeight);
    // Handle open keyboard
  }
};

Testing and Debugging

For the solution to work correctly, thorough testing is important:

1. Developer Tools

Use Chrome/Safari developer tools to simulate mobile devices and debug viewport:

javascript
// For debugging, you can add logging
console.log('Inner height:', window.innerHeight);
console.log('Visual viewport height:', window.visualViewport?.height);
console.log('Keyboard height:', window.innerHeight - (window.visualViewport?.height || window.innerHeight));

2. Testing on Real Devices

Testing on real iOS devices is especially important as simulators may give different results.

3. Error Handling

Add error handling for cases where window.visualViewport is unavailable:

javascript
useEffect(() => {
  if (!window.visualViewport) {
    console.warn('VisualViewport API is not supported');
    return;
  }

  // Main logic
}, [isOpen]);

Sources

  1. GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile · Issue #2849 · shadcn-ui/ui
  2. Stack Overflow: Shadcn Drawer component with Inputs on mobile, keyboard hides inputs or shows a blank space hiding them
  3. Stack Overflow: Issue with Drawer Height in iOS Safari for ShadCN Component
  4. Martijn Hols: How to get the document height in iOS Safari when the on-screen keyboard is open
  5. Saric Den: How to make fixed elements respect the virtual keyboard on iOS
  6. SW Habitation: How to Fix the Annoying White Space Issue in iOS Safari

Conclusion

The problem with empty space under the drawer on mobile devices is solved using the window.visualViewport API, which allows precise tracking of viewport changes when working with the virtual keyboard. The main steps to solve the problem:

  1. Use window.visualViewport.addEventListener to track viewport size changes
  2. Dynamically adjust height and position of drawer elements based on keyboard height
  3. Handle keyboard opening and closing to prevent residual changes
  4. Add error handling for cases where the API is unavailable
  5. Combine with other approaches (updating Vaul, CSS optimization) for better results

This solution is not a “hack” but is based on modern web standards and APIs specifically designed to solve such problems. It will ensure smooth operation of your cart component on all mobile devices without unwanted interface jerks.