| name | modal-media-viewport-overflow |
| description | Fix for content below large media elements (video, image, iframe) in fixed modals
being pushed off-screen and inaccessible. Use when: (1) caption, links, or buttons
below a video/image in a lightbox or modal are invisible despite being in the DOM,
(2) modal uses vh-based max-height on media + body scroll lock and footer content
disappears, (3) elements pass accessibility tree checks but are physically unreachable
because the modal can't scroll. Covers the min-h-0 + flex-shrink-0 + calc(100dvh)
pattern for Tailwind CSS and plain CSS flex column modals.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-03T00:00:00.000Z" |
Modal Media Viewport Overflow
Problem
When building a fixed/fullscreen modal (lightbox, dialog) containing a large media element
(video, image, iframe) with footer content below it (captions, links, action buttons), the
footer content gets pushed below the visible viewport. Since the modal typically locks body
scroll (overflow: hidden), users cannot scroll to reach the hidden content.
The elements are present in the DOM and pass accessibility tree inspections, but they are
physically invisible and unreachable.
Context / Trigger Conditions
- A fixed/fullscreen modal or lightbox component
- Large media element (video, image) with
max-h-[Xvh] height constraint
- Content below the media (caption text, "Watch on YouTube" link, action buttons)
- Body scroll lock:
document.body.style.overflow = 'hidden'
- Outer container uses
justify-center (vertically centers content)
- Symptom: Footer content is in the DOM but not visible on screen
- Symptom: Elements show in browser DevTools / accessibility tree but can't be seen
- Symptom: Works fine on very tall viewports, breaks on shorter ones
Solution
The fix uses three CSS techniques together:
1. Viewport-relative max-height on media (leave room for footer)
Instead of a fixed percentage like max-h-[75vh], use a calc expression that
mathematically guarantees space for the footer content:
.video { max-height: 75vh; }
.video { max-height: calc(100dvh - 10rem); }
Tailwind: max-h-[calc(100dvh-10rem)]
The 10rem budget covers:
- Container padding (~2-4rem top + bottom)
- Gap between media and footer (~0.75rem)
- Caption text (~1.5rem)
- Link/button row (~1.5rem)
Adjust the value based on your actual footer content height.
2. min-h-0 on the media flex child (allow shrinking)
Flexbox items have min-height: auto by default, which prevents them from shrinking
below their intrinsic content height. Override this on the container holding the media:
.video-wrapper { min-height: 0; }
Tailwind: min-h-0
Without this, the flex item refuses to shrink even when the parent has a max-height
constraint, causing overflow.
3. flex-shrink-0 on the footer content (protect from squishing)
Ensure the footer content (caption, links, buttons) never gets compressed to zero:
.footer { flex-shrink: 0; }
Tailwind: flex-shrink-0
This guarantees the video shrinks before the footer content disappears.
4. overflow-y-auto on the modal container (safety net)
Add scrollability to the modal itself as a safety net for very small viewports:
.modal-overlay { overflow-y: auto; }
Tailwind: overflow-y-auto
Complete Pattern (Tailwind CSS)
{}
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center overflow-y-auto p-4 sm:p-8">
{}
<div className="flex flex-col items-center w-full max-w-5xl max-h-[calc(100dvh-3rem)] sm:max-h-[calc(100dvh-5rem)]">
{}
<div className="w-full min-h-0 rounded-xl overflow-hidden">
<video className="w-full h-auto max-h-[calc(100dvh-10rem)] object-contain" controls>
<source src={videoSrc} type="video/mp4" />
</video>
</div>
{}
<div className="flex-shrink-0 mt-3 flex items-center gap-3">
<p>Caption text</p>
<a href={url}>Watch on YouTube</a>
</div>
</div>
</div>
Plain CSS Equivalent
.modal-overlay {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-y: auto;
padding: 1rem;
}
.content-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 64rem;
max-height: calc(100dvh - 3rem);
}
.media-wrapper {
width: 100%;
min-height: 0;
}
.media-wrapper video {
width: 100%;
height: auto;
max-height: calc(100dvh - 10rem);
object-fit: contain;
}
.footer {
flex-shrink: 0;
margin-top: 0.75rem;
}
Verification
Use Playwright or browser DevTools to verify:
const link = await page.locator('[data-testid="youtube-link"]').boundingBox();
const viewport = page.viewportSize();
const linkBottom = link.y + link.height;
console.log(linkBottom <= viewport.height ? 'VISIBLE' : 'CLIPPED');
Test at multiple viewport sizes:
- Desktop: 1280x800
- Tablet: 768x1024
- Mobile: 390x660
- Small mobile: 375x550
Example
From this project's VideoLightbox.tsx — before and after:
Before (broken):
<video className="w-full h-auto max-h-[75vh] object-contain" />
<div className="mt-4">
<p>{caption}</p>
<a href={youtubeUrl}>Watch on YouTube</a> {/* INVISIBLE */}
</div>
After (fixed):
<div className="max-h-[calc(100dvh-3rem)]">
{}
<div className="min-h-0">
<video className="w-full h-auto max-h-[calc(100dvh-10rem)] object-contain" />
</div>
{}
<div className="flex-shrink-0 mt-3">
<p>{caption}</p>
<a href={youtubeUrl}>Watch on YouTube</a> {}
</div>
</div>
Notes
dvh vs vh: Use dvh (dynamic viewport height) for mobile browsers where the
toolbar resizes. Falls back gracefully — all modern browsers support it. On desktop,
dvh equals vh.
justify-center gotcha: When the outer flex container uses justify-center and
content overflows, the top of the content can be clipped above the viewport (not just
the bottom). Adding overflow-y-auto to the container fixes this.
- Body scroll lock: Remember that
document.body.style.overflow = 'hidden' prevents
users from scrolling the page, but overflow-y-auto on the modal overlay still
allows scrolling within the modal.
- Native
<dialog> element: Chrome applies default max-height: calc(100% - 2em - 6px)
to <dialog> elements. If using native dialog, you may need max-height: none to
override this.
- This also applies to images: The same pattern works for image lightboxes with
captions, download buttons, or EXIF info below the image.
- The
min-h-0 fix is the most commonly missed piece — without it, the flex item
won't shrink below its content's intrinsic height even with max-height on the parent.
References