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:
'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;
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
- API
window.visualViewportas a Solution - Practical Solution for Your Component
- Alternative Approaches and Their Comparison
- Optimization for iOS Safari
- Testing and Debugging
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:
- The virtual keyboard appears and covers part of the screen
- iOS Safari changes the visual viewport (what the user sees), but doesn’t change the layout viewport (what determines element sizes)
- The drawer component maintains its original height, which leads to empty space appearing under it
- 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-contentand 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.
// 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:
'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:
- Using
reffor direct access to drawer elements - Tracking
window.visualViewportto determine keyboard height - Dynamically changing height and padding of content
- 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:
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:
<div className="overflow-auto">
<Cart />
</div>
3. Working with CSS Viewport Units
Another approach is using CSS properties that consider dynamic viewport:
.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:
.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:
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:
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:
// 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:
useEffect(() => {
if (!window.visualViewport) {
console.warn('VisualViewport API is not supported');
return;
}
// Main logic
}, [isOpen]);
Sources
- GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile · Issue #2849 · shadcn-ui/ui
- Stack Overflow: Shadcn Drawer component with Inputs on mobile, keyboard hides inputs or shows a blank space hiding them
- Stack Overflow: Issue with Drawer Height in iOS Safari for ShadCN Component
- Martijn Hols: How to get the document height in iOS Safari when the on-screen keyboard is open
- Saric Den: How to make fixed elements respect the virtual keyboard on iOS
- 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:
- Use
window.visualViewport.addEventListenerto track viewport size changes - Dynamically adjust height and position of drawer elements based on keyboard height
- Handle keyboard opening and closing to prevent residual changes
- Add error handling for cases where the API is unavailable
- 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.