
Contents
Peek is a tiny, dependency-free library that hides your site header when a visitor scrolls down and brings it back when they scroll up. Because it’s plain JavaScript that just toggles CSS classes, it drops into anything — but the two platforms I build on most are WordPress and HubSpot CMS, so this guide covers the universal setup first, then exactly how to wire it into each.
No build step, no framework, one file. About ten minutes either way.
The part that’s the same everywhere
Peek never moves your header itself. It watches scroll intent and toggles classes on the element you point it at; your CSS does the actual motion. That means the markup, CSS, and init call below are identical on WordPress, HubSpot, or a static HTML file — only the “where do the files go” part differs, which is what the platform sections cover.
1. A header to target
Start with a fixed or sticky header that has a stable class name:
<header class="main-header">
<!-- logo, nav, etc. -->
</header>
.main-header {
position: fixed;
inset: 0 0 auto 0;
z-index: 100;
transition: transform 0.3s ease;
}
2. The CSS that actually hides it
This is the step people skip. Peek only adds/removes classes — nothing moves until you give those classes a transition. Use transform: translateY(); it’s GPU-accelerated and doesn’t trigger layout reflow:
/* Hidden — slid up out of view */
.main-header--unpinned { transform: translateY(-100%); }
/* Visible — slid back into place */
.main-header--pinned { transform: translateY(0); }
Those are Peek’s default class names. Optionally style the top-vs-scrolled state too — Peek also toggles main-header--top (within offset px of the top) and main-header--not-top (scrolled past it):
.main-header--top { background: transparent; box-shadow: none; }
.main-header--not-top { background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
3. The init call
const peek = Peek('.main-header', {
offset: 150, // px scrolled before Peek engages (default 100)
tolerance: 10 // px of movement that counts as intent (default 5)
});
A higher tolerance ignores small scroll jitter so the header doesn’t flicker on trackpads and touchscreens. If your theme names its header something else, don’t rename your CSS — remap Peek’s classes instead:
Peek('.site-nav', {
classes: { pinned: 'site-nav--pinned', unpinned: 'site-nav--unpinned' }
});
WordPress
Don’t paste a raw <script> tag into your theme — enqueue it so it loads in the footer, deferred, and only once. Drop peek.js into your theme (e.g. assets/js/peek.js) and add this to functions.php:
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'peek',
get_template_directory_uri() . '/assets/js/peek.js',
array(),
'1.0.0',
array( 'in_footer' => true, 'strategy' => 'defer' )
);
// Init inline, attached to the peek handle so it always runs after it.
wp_add_inline_script(
'peek',
"Peek('.site-header', { offset: 120, tolerance: 8 });"
);
} );
Point the selector at whatever your theme calls the header wrapper — .site-header, .site-nav, #masthead, etc. Most block themes expose a stable class on the header template part; classic themes usually have one on the <header>. If yours doesn’t, add one in the header template rather than targeting a fragile auto-generated class. Standard WordPress page loads tear everything down on navigation, so there’s no cleanup to worry about.
HubSpot CMS
Two ways in, depending on how much of the theme you own:
Theme/template level (preferred). Upload peek.js into the Design Manager file system (e.g. /js/peek.js), then load it from your base template or header partial with HubL so HubSpot fingerprints and CDN-serves it:
{{ require_js(get_asset_url("/js/peek.js")) }}
{% require_js position="footer" %}
<script>
Peek('.header-container', { offset: 120, tolerance: 8 });
</script>
{% end_require_js %}
Site-wide quick win (no template access). Settings → Content → Pages → Site footer HTML: paste the <script src> for peek.js followed by the one-line init. It loads on every page without touching templates.
For the selector: HubSpot’s global header is a header partial or menu module, and the wrapper class is theme-dependent — .header-container, .header__container, or whatever your theme uses. Inspect the rendered header, or add your own class to the header module’s wrapper in the template, then target that. Same principle as WordPress: target a class you control, not a generated one.
One note on single-page navigation
Standard WordPress and HubSpot pages do a full reload on navigation, so Peek’s listeners die with the page — nothing to manage. If you’re running an SPA layer, Turbo, or HubSpot with client-side route swapping, call peek.destroy() before the header is torn down so listeners don’t stack up:
peek.destroy();
That’s the whole thing
One file, one init call, and a transition — wired into WordPress with a proper enqueue or into HubSpot with require_js. No framework, no dependencies, and a behavior you can read end to end. Peek is on GitHub under MIT.