> For the complete documentation index, see [llms.txt](https://docs.nickarce.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nickarce.com/customization/how-tos/add-a-banner-above-the-header.md).

# Add a Banner Above the Header

The announcement bar scrolls up and out of view while the main header follows it up, then stops and pins below the top of the screen. No gap, no jump, no double-height header.

This works whether the header resolves to `position: sticky` or `position: fixed`, so toggling `data-overlay-header` will not break it.

### Before you start

Your header needs this structure, with the banner **inside** the Header Wrapper:

```html
<header class="header-wrapper">   <!-- behavior shell, has the attached script -->
  <div class="header-banner">     <!-- announcement bar -->
  <div class="header">            <!-- logo + nav -->
```

### Step 1: Understand why it doesn't work by default

`.header-wrapper` is the positioned box, so everything inside rides along with it, banner included.

The fix is to offset the wrapper upward by exactly the banner's height. The banner ends up above the viewport while `.header` lands at the top.

How much work that takes depends on which position the wrapper resolves to:

| Attributes       | Computed position | Who handles the timing                           |
| ---------------- | ----------------- | ------------------------------------------------ |
| Sticky only      | `sticky`          | The browser. A static negative offset is enough. |
| Sticky + overlay | `fixed`           | You. Fixed never waits for scroll.               |

`position: sticky` waits for scroll natively. `position: fixed` does not, so a negative offset applied to it takes effect at first paint and the banner would be gone before the visitor scrolls at all.

This guide supports both by publishing two CSS variables and letting each rule use only what it needs.

### Step 2: Add the JavaScript

Paste this into the Header Wrapper's JavaScript panel at the very bottom. It is fully self-contained, so it can go anywhere in the file and nothing existing needs editing.

```javascript
// BANNER HEIGHT + SCROLL OFFSET
// Publishes two custom properties:
//   --banner-height    the banner's measured height
//   --header-scroll-y  scroll position, clamped to that height
// The sticky rule uses only the first. The fixed rule uses both.
(() => {
  const bannerEl = document.querySelector('.header-banner');
  if (!bannerEl) return;

  const root = document.documentElement;
  let bannerHeight = 0;
  let lastY = -1;
  let ticking = false;

  const updateScroll = () => {
    ticking = false;
    // Clamped, so once you're past the banner we stop writing entirely
    const y = Math.min(Math.max(0, window.scrollY), bannerHeight);
    if (y === lastY) return;
    lastY = y;
    root.style.setProperty('--header-scroll-y', `${y}px`);
  };

  const measureBanner = () => {
    bannerHeight = bannerEl.offsetHeight;
    root.style.setProperty('--banner-height', `${bannerHeight}px`);
    lastY = -1; // force a re-write against the new height
    updateScroll();
  };

  const onScroll = () => {
    if (!ticking) {
      ticking = true;
      requestAnimationFrame(updateScroll);
    }
  };

  if (window.ResizeObserver) {
    // border-box so padding/border changes at breakpoints re-measure too
    new ResizeObserver(measureBanner).observe(bannerEl, { box: 'border-box' });
  } else {
    window.addEventListener('resize', measureBanner);
    window.addEventListener('load', measureBanner);
  }

  window.addEventListener('scroll', onScroll, { passive: true });
  measureBanner();
})();
```

Four details worth understanding:

* `if (!bannerEl) return;` means pages without a banner exit immediately and keep stock behavior.
* The `requestAnimationFrame` throttle collapses scroll events down to one write per frame, since scroll fires far more often than the screen repaints.
* The clamp plus the `lastY` guard means writes stop completely once you are past the banner. You pay for roughly the first 76px of scroll and nothing after.
* `{ box: 'border-box' }` matters because the ResizeObserver default is content-box, which ignores padding and border changes.

### Step 3: Replace the Overlay & Sticky Header CSS

Open the Header Wrapper's CSS panel and find the `/* ## Overlay & Sticky Header */` section. Use the comment navigation menu at the top left of the CSS editor to jump straight to it.

Replace everything from that comment down to (but not including) `/* ### hide on scroll */` with the block below.

```css
  /* ## Overlay & Sticky Header */
  &[data-overlay-header='true'],
  &[data-sticky-header='true'] {
    inset-block-start: var(--wp-admin--admin-bar--height, 0px);
  }

  &[data-overlay-header='true'] {
    position: absolute;
  }

  &[data-sticky-header='true'] {
    position: sticky;
  }

  /* Sticky without overlay.
     Position comes from the rule above; this only shifts the offset up by
     the banner's height. Sticky handles the timing natively, so
     --header-scroll-y is deliberately not used here. */
  &[data-sticky-header='true']:not([data-overlay-header='true']) {
    inset-block-start: calc(
      var(--wp-admin--admin-bar--height, 0px) - var(--banner-height, 0px)
    );
    /* Optional: reclaim the flow space so content starts at the top of the
       page, the way overlay does. Delete this line for normal flow. */
    margin-block-end: calc(-1 * var(--header-height, 0px));
  }

  /* Sticky + overlay resolves to fixed. A fixed box cannot wait for scroll
     the way sticky does, so min() supplies that timing from
     --header-scroll-y and caps the movement at the banner's height. */
  &[data-sticky-header='true'][data-overlay-header='true'] {
    position: fixed;
    inset-block-start: calc(
      var(--wp-admin--admin-bar--height, 0px) -
      min(var(--header-scroll-y, 0px), var(--banner-height, 0px))
    );
  }

  &[data-overlay-header='true'][data-overlay-header-offset='true'] + main > :first-child > :first-child {
    margin-block-start: var(--header-height);
  }
```
