skills/hubeiqiao/skills/modal-media-viewport-overflow

modal-media-viewport-overflow

Installation
SKILL.md

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:

/* Bad: leaves no room for footer */
.video { max-height: 75vh; }

/* Good: guarantees 10rem for padding + footer */
.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)

{/* Outer modal overlay — scrollable safety net */}
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center overflow-y-auto p-4 sm:p-8">

  {/* Content wrapper — bounded to viewport */}
  <div className="flex flex-col items-center w-full max-w-5xl max-h-[calc(100dvh-3rem)] sm:max-h-[calc(100dvh-5rem)]">

    {/* Media container — min-h-0 allows flex shrink */}
    <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>

    {/* Footer — flex-shrink-0 guarantees visibility */}
    <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; /* Critical: allows flex shrink */
}

.media-wrapper video {
  width: 100%;
  height: auto;
  max-height: calc(100dvh - 10rem);
  object-fit: contain;
}

.footer {
  flex-shrink: 0; /* Critical: never hide this */
  margin-top: 0.75rem;
}

Verification

Use Playwright or browser DevTools to verify:

// Check that footer content is within the viewport
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 at 75vh pushes caption + YouTube link off-screen
<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):

// Content wrapper bounded to viewport
<div className="max-h-[calc(100dvh-3rem)]">
  {/* Video shrinks to fit, min-h-0 on wrapper */}
  <div className="min-h-0">
    <video className="w-full h-auto max-h-[calc(100dvh-10rem)] object-contain" />
  </div>
  {/* Footer guaranteed visible */}
  <div className="flex-shrink-0 mt-3">
    <p>{caption}</p>
    <a href={youtubeUrl}>Watch on YouTube</a>  {/* VISIBLE */}
  </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

Weekly Installs
1
First Seen
7 days ago