0px mobile

Mobile Overlay Header — Implementation Guide

A header that collapses into an expandable overlay on mobile and renders as a full static bar on desktop (1200px+). Demonstrated with an order tracking context. Resize the browser window past 1200px to see both modes.

1

What This Pattern Does

On desktop (1200px and wider) the header bar is a normal, always-visible block element. It sits in the document flow and pushes page content down below it.

On mobile (under 1200px) the header content is hidden by default. A compact toggle button appears in the top-right corner of the bar. Clicking it expands the header content as a drop-down panel that overlays the page rather than reflowing it, then dismisses on a second click or an outside click.

The two properties that create the overlay effect are position: absolute on the panel (mobile) and position: static on the panel (desktop). The toggle button's animated width — 33% when closed, 100% when open — is a secondary visual cue driven by a transition: width rule.

2

Live Demo

Try it: Narrow the browser below 1200px to activate mobile mode. The toggle button appears at the top-right. Click it to expand or collapse the order details. Click anywhere outside the panel to dismiss it. The badge in the top-right corner shows your current viewport width.
Order Number #ORD-2024-00847
Item Ergonomic Office Chair Model XC-9000 — Charcoal
Status In Transit
Est. Delivery Jan 15, 2025
Carrier / Tracking FastShip Logistics 1Z999AA1234567890

Page content lives here.

On mobile the expanded panel floats over this text — it does not push it down. On desktop the header above is always visible and part of the normal page flow.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.

3

HTML Structure

Three structural layers make up the pattern. Each has a single responsibility: the outer wrapper anchors the overlay, the toggle button triggers it, and the panel holds the content.

Vanilla JS version (no framework required)
<!--
    LAYER 1 — OUTER WRAPPER
    position: relative on this element anchors the absolutely-positioned
    panel so it drops directly below this bar (not the page body).
    display: flex + justify-content: flex-end keep the toggle button
    right-aligned when it is in its narrow (33%) collapsed state.
-->
<div class="order-header-outer">

    <!--
        LAYER 2 — TOGGLE BUTTON  (hidden on desktop via CSS)
        Starts with .collapsed — panel is closed by default.
        JavaScript removes .collapsed when open; re-adds it when closed.
        aria-expanded / aria-controls provide accessibility context.
    -->
    <button class="order-header-toggle collapsed"
            id="orderToggleBtn"
            type="button"
            aria-expanded="false"
            aria-controls="orderHeaderPanel">
        Order Details
    </button>

    <!--
        LAYER 3 — PANEL
        Mobile:  position:absolute; overlays the page below the toggle.
                 max-height:0 + overflow:hidden keeps it invisible.
                 JavaScript adds .show (max-height:600px) to reveal it.
        Desktop: position:static; always visible; part of normal flow.
    -->
    <div class="order-header-panel" id="orderHeaderPanel">
        <div class="order-header-container">

            <!-- One .order-header-frame per data field -->
            <div class="order-header-frame">
                <span class="order-header-label">Order Number</span>
                <span class="order-header-value">#ORD-2024-00847</span>
            </div>

            <!-- Repeat for each additional field -->

        </div>
    </div>

</div>
Bootstrap 4 version (replaces the JS toggle)
<!--
    Bootstrap handles open/close automatically via data-toggle.
    d-xl-none  hides the button  at the xl breakpoint (1200px+).
    d-xl-block forces the panel visible at the xl breakpoint.
    The .collapsed class is managed by Bootstrap's Collapse component.
-->
<button class="order-header-toggle collapsed d-xl-none"
        type="button"
        data-toggle="collapse"
        data-target="#orderHeaderPanel"
        aria-expanded="false"
        aria-controls="orderHeaderPanel">
    Order Details
</button>

<div class="collapse d-xl-block order-header-panel" id="orderHeaderPanel">
    ...
</div>

<!--
    With Bootstrap you can remove the display:none rule for .order-header-toggle
    and the position:static / max-height:none overrides for .order-header-panel
    from your stylesheet, because d-xl-none and d-xl-block handle them.
-->
4

CSS — Base Styles (Mobile-First)

These rules apply at all viewport widths. The desktop media query in Section 5 selectively overrides a handful of them.

.order-header-outer
.order-header-outer {

    position: relative;
    /*
     * Creates a positioning context so that position:absolute on the panel
     * is calculated from THIS element's edges, not from the nearest
     * positioned ancestor elsewhere in the DOM.
     * Without this, top:100% on the panel would not mean "below the toggle bar."
     */

    display: flex;
    /*
     * Activates flexbox so that justify-content below has an effect.
     * On mobile this wrapper contains only the toggle button as a visible child.
     */

    justify-content: flex-end;
    /*
     * Pushes the toggle button to the right edge.
     * When the button is in its collapsed (33%-wide) state it appears as a
     * compact tab in the corner rather than a left-aligned strip.
     */
}
.order-header-panel
.order-header-panel {

    position: absolute;
    /*
     * Lifts the panel out of the normal document flow entirely.
     * It will not push down sibling elements — it floats on top of them.
     * This is the property that gives the panel its "overlay" behavior.
     */

    top: 100%;
    /*
     * 100% equals the full height of the containing block (.order-header-outer).
     * This places the top edge of the panel immediately below the toggle button.
     */

    left: 0;
    right: 0;
    /*
     * Stretches the panel to the full width of .order-header-outer.
     * Together these two properties make the panel as wide as the bar itself.
     */

    z-index: 200;
    /*
     * Stacks the panel above other page content when it is open.
     * Raise this number if modals, sticky navbars, or other overlays
     * appear in front of the panel unexpectedly.
     */

    max-height: 0;
    /*
     * Collapses the panel to zero height, making it invisible.
     * This is the "hidden" default state on mobile.
     * Works in tandem with overflow:hidden below.
     */

    overflow: hidden;
    /*
     * Clips any content that extends beyond max-height:0.
     * Without this the content would remain visible even at max-height:0
     * because it would just spill outside the element's bounds.
     */

    transition: max-height 0.35s ease;
    /*
     * Animates the height change when JavaScript adds or removes .show.
     * The panel slides smoothly into view rather than appearing instantly.
     * The timing (0.35s) should match the toggle button's width transition.
     */
}

.order-header-panel.show {

    max-height: 600px;
    /*
     * JavaScript adds this class to reveal the panel.
     * Set this value larger than the panel will ever actually be — the
     * animation runs from 0 to this number, but stops visually as soon as
     * the content ends. Too small: content is clipped. Too large: the
     * animation eases out too slowly on short content. 600px is a safe
     * default for a compact info bar; adjust for taller panels.
     */
}
.order-header-toggle
.order-header-toggle {

    display: flex;
    align-items: center;
    justify-content: flex-end;
    /*
     * Lays out the label text and icon side by side, right-aligned.
     * This keeps the text/icon position consistent as the button
     * animates between its wide (100%) and narrow (33%) states.
     */

    gap: 15px;
    /*
     * Space between the label text ("Order Details") and the +/- icon.
     */

    width: 100%;
    /*
     * Full width when the panel is expanded (no .collapsed class).
     * The width transitions smoothly; see transition below.
     */

    border: none;
    /*
     * Removes the default browser button border. Essential for a
     * clean bar appearance — without this a rectangle outline appears.
     */

    padding: 10px 20px;
    cursor: pointer;

    white-space: nowrap;
    /*
     * Prevents the label text from wrapping to a new line during
     * the width animation. Without this the text reflows awkwardly
     * as the button shrinks from 100% to 33%.
     */

    min-width: max-content;
    /*
     * Ensures the button never becomes narrower than its own text content.
     * Acts as a floor so the label is never cut off during the animation.
     * min-width: max-content means "be at least as wide as the content."
     */

    transition: width 0.35s ease;
    /*
     * Smoothly animates the width change between 100% (open) and
     * 33% (collapsed). The duration matches the panel's max-height
     * transition so both animations finish at the same time.
     */
}

.order-header-toggle.collapsed {

    width: 33%;
    /*
     * Compact tab size when the panel is hidden.
     * Because .order-header-outer uses justify-content:flex-end,
     * this 33%-wide button sits anchored to the right side of the bar.
     * JavaScript adds/removes .collapsed on click.
     */
}

.order-header-toggle::after {

    content: "\2212"; /* − minus sign */
    /*
     * A CSS pseudo-element adds the expand/collapse icon without
     * requiring any extra HTML element.
     * The minus sign is shown when the panel is OPEN.
     */

    font-size: 22px;
    line-height: 1;
}

.order-header-toggle.collapsed::after {

    content: "\002B"; /* + plus sign */
    /*
     * Swaps to a plus sign automatically whenever .collapsed is present.
     * No JavaScript needed to change the icon — it follows the class.
     */
}
.order-header-container and .order-header-frame
.order-header-container {

    display: flex;
    flex-direction: column;
    /*
     * On mobile: stacks each data frame (label + value pair) vertically.
     * The desktop media query overrides this to flex-direction:row.
     */

    align-items: flex-start;
    row-gap: 22px;
    /*
     * Vertical space between each frame. row-gap only applies in
     * flex-direction:column; in row direction the gap property is used instead.
     */

    padding: 18px 24px 24px;

    overflow-wrap: anywhere;
    word-break: break-word;
    /*
     * Allows long strings — order numbers, tracking IDs — to wrap at any
     * character instead of overflowing their container.
     * overflow-wrap:anywhere is modern; word-break:break-word is the
     * older-browser fallback that achieves the same result.
     */
}

.order-header-frame {

    display: flex;
    flex-direction: column;
    /*
     * The label sits above the value in a vertical column.
     */

    align-items: flex-start;
    gap: 4px;

    min-width: 0;
    /*
     * A common flexbox fix: flex items default to min-width:auto which
     * can cause them to overflow their container when content is wide.
     * Setting min-width:0 allows the item to shrink past its content size.
     */

    max-width: 100%;
}

.order-header-frame:empty {

    display: none;
    /*
     * Automatically hides any frame element with no child content.
     * Useful when fields are conditionally rendered server-side — an
     * empty frame would otherwise leave a visible gap in the layout.
     */
}
5

CSS — Desktop Breakpoint (1200px+)

At 1200px and above the overlay behavior is entirely disabled. The panel becomes a permanent in-flow element and the toggle disappears. Only a handful of properties need to change; everything else from the base styles still applies.

@media (min-width: 1200px) {

    .order-header-outer {
        display: block;
        /*
         * Reverts from flex to block. The toggle button is hidden here
         * so there is nothing left to align; flex is no longer needed.
         */
    }

    .order-header-panel {
        position: static;
        /*
         * Returns the panel to normal document flow. It is no longer an
         * overlay — it sits inline and pushes content below it down
         * the page like any other block element.
         */

        max-height: none;
        /*
         * Removes the height restriction entirely so the panel is always
         * fully visible regardless of whether JavaScript ever added .show.
         * This means the desktop header works even with JS disabled.
         */

        overflow: visible;
        /*
         * No longer needs to clip content. Redundant once max-height is
         * none, but explicit is clearer when reading the stylesheet.
         */

        transition: none;
        /*
         * Disables the slide animation at desktop widths. There is nothing
         * to animate because the panel is permanently open. Leaving the
         * transition active here would cause a slow fade-in on page load.
         */
    }

    .order-header-toggle {
        display: none;
        /*
         * Hides the toggle button entirely. The header content is always
         * visible on desktop so a toggle is unnecessary.
         * If using Bootstrap, d-xl-none on the button handles this instead,
         * and this rule can be omitted from your stylesheet.
         */
    }

    .order-header-container {
        flex-direction: row;
        /*
         * Switches from the mobile vertical stack to a horizontal row
         * so data frames sit side by side across the full bar width.
         */

        justify-content: space-between;
        /*
         * Distributes frames evenly, pushing the first to the far left
         * and the last to the far right with equal space between them.
         */

        align-items: center;
        /*
         * Vertically centers frames that may have different heights
         * (e.g., a frame with two lines vs. one line).
         */

        flex-wrap: nowrap;
        /*
         * Forces all frames onto a single row. If content overflows at
         * narrow desktop sizes, reduce the gap or font sizes rather than
         * allowing wrapping, which breaks the bar layout.
         */

        padding: 10px 36px;
        gap: 36px;

        row-gap: 0;
        /*
         * Resets the mobile row-gap. In flex-direction:row the row-gap
         * property is irrelevant, but zeroing it is explicit and avoids
         * confusion when reading or debugging the stylesheet.
         */

        min-height: 80px;
        /*
         * Gives the bar a consistent minimum height on desktop so it does
         * not collapse when frames are short.
         */
    }

    .order-header-frame {
        flex: 0 1 auto;
        /*
         * flex-grow:0   — frame will NOT stretch to fill extra space.
         * flex-shrink:1 — frame CAN shrink if the bar is too narrow.
         * flex-basis:auto — frame starts at its natural content width.
         * This prevents any single frame from dominating the row width.
         */

        min-width: 0;
        /*
         * Still needed on desktop for the same reason as mobile:
         * prevents flex children from overflowing their container.
         */
    }

}
6

JavaScript — Toggle & Outside-Click

Two behaviors need JavaScript: toggling the panel open/closed on button click, and closing the panel when the user clicks anywhere outside it. Both are wrapped in an IIFE so no globals are introduced.

Vanilla JS — no dependencies
(function () {

    var xlBreakpoint = 1200;

    /* Returns true when the viewport is narrower than the desktop breakpoint. */
    function isMobile() {
        return window.innerWidth < xlBreakpoint;
    }


    /* ── TOGGLE BUTTON ─────────────────────────────────────────────────── */

    document.querySelectorAll('.order-header-toggle').forEach(function (btn) {

        /*
         * Read the panel ID from the button's aria-controls attribute
         * rather than hard-coding it, so the same script works for
         * multiple independent header bars on the same page.
         */
        var panelId = btn.getAttribute('aria-controls');
        var panel   = document.getElementById(panelId);

        btn.addEventListener('click', function () {

            var isCollapsed = btn.classList.contains('collapsed');

            if (isCollapsed) {
                /* EXPAND — show panel, update button state and ARIA. */
                panel.classList.add('show');
                btn.classList.remove('collapsed');
                btn.setAttribute('aria-expanded', 'true');
            } else {
                /* COLLAPSE — hide panel, update button state and ARIA. */
                panel.classList.remove('show');
                btn.classList.add('collapsed');
                btn.setAttribute('aria-expanded', 'false');
            }
        });
    });


    /* ── OUTSIDE-CLICK TO CLOSE ─────────────────────────────────────────── */

    document.addEventListener('click', function (e) {

        /*
         * On desktop the panel is always visible — nothing to dismiss.
         * The early return avoids unnecessary DOM work on every click.
         */
        if (!isMobile()) { return; }

        document.querySelectorAll('.order-header-panel.show').forEach(function (panel) {

            /*
             * Walk up from the panel to its .order-header-outer wrapper.
             * If the click target is NOT inside that wrapper, close the panel.
             * This keeps clicks inside the panel (links, buttons) from
             * accidentally triggering a collapse.
             */
            var outer = panel.closest('.order-header-outer');

            if (outer && !outer.contains(e.target)) {
                panel.classList.remove('show');

                var btn = outer.querySelector('.order-header-toggle');
                if (btn) {
                    btn.classList.add('collapsed');
                    btn.setAttribute('aria-expanded', 'false');
                }
            }
        });
    });

})();
Bootstrap 4 equivalent (outside-click only)
/*
 * Bootstrap handles open/close automatically via data-toggle="collapse".
 * Only the outside-click dismiss needs a custom handler.
 */
(function () {

    var xlBreakpoint = 1200;

    function isMobile() {
        return window.innerWidth < xlBreakpoint;
    }

    document.addEventListener('click', function (e) {
        if (!isMobile()) { return; }

        [].slice.call(document.querySelectorAll('.order-header-panel')).forEach(function (el) {
            if (!el.classList.contains('show')) { return; }

            var outer = el.closest('.order-header-outer');
            if (outer && !outer.contains(e.target)) {
                $(el).collapse('hide');  /* Bootstrap jQuery API */
            }
        });
    });

})();
7

How It All Fits Together

Mobile — closed: The outer wrapper is a flex row. The toggle button (33% wide, right-aligned) is visible. The panel has max-height:0; overflow:hidden making it invisible. Because the panel is position:absolute it takes up no vertical space even when open.

Mobile — open: JavaScript adds .show to the panel (max-height:600px) and removes .collapsed from the button (width:100%). Both CSS transitions run simultaneously over 0.35s. The panel slides down as an overlay above page content. Clicking outside fires the outside-click handler, which reverses both changes.

Desktop: The media query fires. The toggle is hidden (display:none). The panel is position:static; max-height:none; overflow:visible — permanently visible regardless of JS state. The container becomes a horizontal row. The outside-click handler exits immediately because isMobile() returns false.

Class / Modifier Responsibility
.order-header-outer Positioning anchor (position:relative) and flex host for right-aligning the collapsed toggle button.
.order-header-panel The expandable content area. Absolute overlay on mobile (position:absolute; top:100%), static element on desktop.
.order-header-panel.show Added by JavaScript to reveal the panel. Sets max-height:600px to trigger the slide-down transition.
.order-header-toggle Mobile-only button. Animates between 33% and 100% width via transition:width. Hidden with display:none at 1200px+.
.order-header-toggle.collapsed Modifier managed by JavaScript. Shrinks the button to 33% and swaps the ::after pseudo-element from − to +.
.order-header-container Inner flex container. Vertical column on mobile; horizontal row on desktop via the 1200px media query.
.order-header-frame Wraps a label + value pair. :empty auto-hides frames with no server-rendered content. min-width:0 prevents flex overflow.