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:
'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;
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
- Solution 1: Update Vaul and Proper Overflow Handling
- Solution 2: Dynamic Viewport Height Handling
- Solution 3: CSS Fixes for Proper Display
- Optimal Implementation for Your Component
- Recommendations for Preventing Issues
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:
'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:
'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:
/* 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:
'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
-
Update dependencies: Keep an eye on Vaul and shadcn/ui updates, as developers are actively working on mobile fixes.
-
Test on real devices: Issues often only appear on actual mobile devices, not in simulators.
-
Use CSS variables: Add CSS variables for more flexible configuration:
: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);
}
- Error handling: Add handling for browsers that don’t support
visualViewport:
const supportsVisualViewport = 'visualViewport' in window;
- Performance optimization: Use
useCallbackandmemoto prevent unnecessary re-renders:
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
- GitHub Issue: Drawer Input Obstructed by Keyboard in Mobile
- Stack Overflow: Shadcn Drawer component with Inputs on mobile
- Stack Overflow: Issue with Drawer Height in iOS Safari
- r/nextjs: Drawer vaul shadcn mobile issues
- r/shadcn: Drawer by vaul not responsive on mobile with forms
- 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:
- Using the
visualViewportAPI for accurate determination of visible area dimensions - Dynamic height adjustment of the drawer based on keyboard presence
- Proper overflow handling to prevent empty space from appearing
- 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.