# Modal Media Viewport Overflow

> 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.

- Skill: `hubeiqiao/modal-media-viewport-overflow` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hubeiqiao/modal-media-viewport-overflow`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hubeiqiao/modal-media-viewport-overflow/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: hubeiqiao (https://skillmd.com/u/hubeiqiao)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hubeiqiao/modal-media-viewport-overflow

---


# 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:

```css
/* 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:

```css
.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:

```css
.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:

```css
.modal-overlay { overflow-y: auto; }
```

Tailwind: `overflow-y-auto`

### Complete Pattern (Tailwind CSS)

```tsx
{/* 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

```css
.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:

```javascript
// 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):**
```tsx
// 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):**
```tsx
// 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

- [Headless UI Dialog Discussion — Fixed height/width and scrollable content](https://github.com/tailwindlabs/headlessui/discussions/412)
- [Flexbox Modal Issue: Can't Scroll to Top of Overflowing Flex Item](https://www.codestudy.net/blog/can-t-scroll-to-top-of-flex-item-that-is-overflowing-container/)
- [A Responsive Modal With Flexbox and No Media Queries](https://felipperegazio.com/posts/a-responsive-modal-with-flexbox-and-no-media-queries/)
- [Considerations for Styling a Modal — CSS-Tricks](https://css-tricks.com/considerations-styling-modal/)
- [Styling an HTML dialog modal to take the full height of the viewport](https://til.simonwillison.net/css/dialog-full-height)
- [Making Perfectly Sized, Centered, Scrollable Modals](https://medium.com/@ResearchNowEng/making-perfectly-sized-centered-scrollable-modals-a36da09b68e6)

