Gatefold Documentation 1.1

Overlay

HTML Markup

A panel that slides in from the top or bottom edge and behaves as a modal dialog while it is open. A trigger and a panel are paired by one shared id; nothing else connects them.


<!-- Anything can be a trigger, as long as it carries the id. -->
<button class="cel-overlay-trigger" data-cel-overlay-id="portfolio">
  Open
</button>

<div class="cel-overlay" data-cel-overlay-id="portfolio">
  <div class="cel-overlay-item">

    <div class="cel-overlay-media">
      <img src="assets/images/demo/portfolio/portfolio-1-1024w.webp" alt="">
    </div>

    <div class="cel-overlay-meta">
      <span class="cel-label">01 / 08</span>
      <button class="cel-overlay-close" type="button" aria-label="Close">
        <i data-lucide="x"></i>
      </button>
    </div>

    <div class="cel-overlay-body">
      <div class="cel-overlay-scroll">
        <div class="cel-overlay-content">
          <h2>Title</h2>
          <p>Body copy.</p>
        </div>
      </div>
    </div>

  </div>
</div>
Hook Where Purpose
data-cel-overlay-id trigger and panel The pairing. Any number of triggers may share one id.
.cel-overlay-trigger the control Clicking it opens the panel with the matching id.
.cel-overlay the panel The dialog itself.
.cel-overlay-close inside the panel Closes the panel it sits in. No id needed — it looks upward.
data-cel-overlay-direction the panel Written by the script, read by CSS. Do not set it by hand.

The overlay knows nothing about what it contains. Anything that has to react — a gallery stepping through items, analytics, a video that should pause — listens for the events below instead of being wired into this module.

celOverlay.init()

Binds every trigger and close button on the page, and puts every closed panel into the correct ARIA state before anything is clicked.


Initialization

celOverlay.init();

Called for you on DOMContentLoaded, at the bottom of main.js. Triggers and close buttons are marked once and skipped on a second call, so re-running it after inserting markup binds only the new controls. Pass the same options again when you do: each call replaces the previous ones.

Custom Configuration

Anything passed to init() becomes the default for every panel on the page — edit the call in main.js. A single panel can still override it — see open().

Option Default Description
direction 'bottom' Edge the panel enters from. 'top' or 'bottom'.
fill '90%' Height on desktop. CSS forces full height on small screens. Gatefold passes calc(100% - var(--cel-header-h)), so the panel stops exactly at the bottom of the site header and the visible band is the header itself, balanced at any window height.
duration 0.6 Open duration, in seconds.
ease 'power3.out' Open easing.
closeDuration 0.45 Close duration. Shorter than the open on purpose.
closeEase 'power2.in' Close easing.
closeOnEscape true Escape closes the panel.
closeOnOutsideClick true A click outside closes the panel.
lockScroll true Prevents the page behind from scrolling.
trapFocus true Traps Tab inside the panel and marks the rest of the page inert.

celOverlay.open() / close()

Open or close a panel from your own code, by id.


celOverlay.open('portfolio');
celOverlay.close('portfolio');

// Per-panel overrides, merged over whatever init() was given
celOverlay.open('portfolio', { direction: 'top', fill: '100%' });

Events

Both are dispatched on the panel element and bubble, so you can listen on the panel or on document.

Event Fired Detail
cel:overlay-open as the entrance begins { id }
cel:overlay-close after the exit completes { id }

Behavior

  • Opening a second panel closes the first. One overlay is open at a time.
  • Re-opening mid-close cancels the exit and brings the panel back, rather than finishing the close and starting a fresh open. The scroll lock and the handlers are still live at that point, so nothing is locked or bound twice.
  • Focus returns to whatever was focused before the panel opened.
  • Direction and height go to CSS, not to inline styles — an inline height would beat the media query that makes the panel full-height on small screens.
  • Reduced motion collapses the durations to zero rather than skipping the tween, so the completion callbacks still run and the state stays consistent.

Scroll Lock, Inert and Focus Trap

Three things the overlay does to the rest of the page while it is open. They are what make it a dialog rather than a box that happens to sit on top.


Scroll lock

The page behind cannot scroll. Two details matter more than they look:

  • The lock is counted, not a boolean. A close overlapping an open must not unlock the page while something still needs it locked. The count is clamped at zero so a stray extra unlock cannot desync every pair that follows.
  • The scrollbar's width is measured and given back as padding. Hiding the scrollbar reclaims its width and the page jumps sideways by exactly that much. On macOS and iOS the scrollbar is an overlay and takes no width, so the measurement is 0 and nothing is added.

Inert background

Everything outside the panel becomes unfocusable, unclickable and invisible to assistive technology. The script walks from the panel up to <body> marking every sibling on the way, and remembers exactly what it marked — so closing reverts those and nothing else. An element some other code made inert stays inert.

The background is made reachable again as the exit starts, not when it finishes: the page behind should respond the moment the panel is on its way out.

Focus trap

  • The panel itself takes focus, not its first control. A screen reader then announces the dialog rather than “Close, button” with no context, and no focus ring appears around the close button for someone who arrived by mouse.
  • The focusable list is rebuilt on every Tab, so content revealed after opening is included and anything hidden meanwhile is skipped.
  • Inert elements are excluded explicitly, which is not the same test as visibility. An inert element is still laid out, so it passes a visibility check, but the browser skips it when Tab is actually pressed. Left in the list, the trap would compute a first or last control that can never be reached.
  • Shift+Tab from the panel goes to the end of the dialog. There is nowhere else for it to go — the rest of the page is inert.

role="dialog", aria-modal, aria-hidden and the triggers' aria-expanded are all maintained for you. Closed panels are given aria-hidden at init rather than after the first open.