feat: animate mobile overlay panel entrance

Slide the overlay panel up from the bottom with a scrim fade on open, matching
the surface sheet entrance.
This commit is contained in:
Bohdan Triapitsyn
2026-06-02 20:33:13 +03:00
parent 7adbaca797
commit d0623705b1
@@ -16,6 +16,9 @@ interface MobileOverlayPanelProps {
} }
const OVERLAY_ROOT_ID = 'mobile-overlay-root'; const OVERLAY_ROOT_ID = 'mobile-overlay-root';
// Entrance animation: classic slide up from the bottom + scrim fade.
const ENTER_DELAY_MS = 16;
const ENTER_DURATION_MS = 200;
const ensureOverlayRoot = () => { const ensureOverlayRoot = () => {
if (typeof document === 'undefined') return null; if (typeof document === 'undefined') return null;
@@ -39,11 +42,22 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
renderHeader, renderHeader,
}) => { }) => {
const overlayRootRef = React.useRef<HTMLElement | null>(null); const overlayRootRef = React.useRef<HTMLElement | null>(null);
const [entered, setEntered] = React.useState(false);
if (typeof document !== 'undefined' && !overlayRootRef.current) { if (typeof document !== 'undefined' && !overlayRootRef.current) {
overlayRootRef.current = ensureOverlayRoot(); overlayRootRef.current = ensureOverlayRoot();
} }
// Replay the enter transition on each open (rise + scrim fade).
React.useEffect(() => {
if (!open) {
setEntered(false);
return;
}
const id = window.setTimeout(() => setEntered(true), ENTER_DELAY_MS);
return () => window.clearTimeout(id);
}, [open]);
React.useEffect(() => { React.useEffect(() => {
if (!open) { if (!open) {
return; return;
@@ -70,7 +84,10 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
const content = ( const content = (
<div <div
className="fixed inset-0 z-[60] flex flex-col bg-[rgb(0_0_0_/_0.45)]" className={cn(
'fixed inset-0 z-[60] flex flex-col bg-[rgb(0_0_0_/_0.45)] transition-opacity duration-200 ease-out',
entered ? 'opacity-100' : 'opacity-0',
)}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
onClick={onClose} onClick={onClose}
@@ -81,6 +98,10 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
'mx-auto max-w-lg', 'mx-auto max-w-lg',
className className
)} )}
style={{
transform: entered ? 'none' : 'translateY(100%)',
transition: `transform ${ENTER_DURATION_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`,
}}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >
{(() => { {(() => {