Global UI: Header and Navigation Intermediate 10 min read

Sticky Header and Announcements

Implement sticky headers with scroll behavior, announcement bars with rotation, and dismissible notices for enhanced user experience.

A sticky header keeps navigation accessible as users scroll. Combined with an announcement bar, you can highlight promotions while maintaining easy navigation. Let’s build both with proper scroll handling.

Basic Sticky Header

The simplest sticky header uses CSS:

.header {
position: sticky;
top: 0;
z-index: 100;
background: var(--color-background);
}

That’s it for basic functionality. The header sticks to the top when scrolling.

Sticky with Offset

If you have an announcement bar above the header:

<div class="announcement-bar">
Free shipping on orders over $50
</div>
<header class="header">
<!-- header content -->
</header>
.announcement-bar {
background: var(--color-primary);
color: var(--color-primary-text);
text-align: center;
padding: var(--spacing-sm);
}
.header {
position: sticky;
top: 0;
z-index: 100;
}

The header will stick after the announcement scrolls away.

Hide on Scroll Down

A popular pattern hides the header when scrolling down and shows it when scrolling up:

class StickyHeader extends HTMLElement {
constructor() {
super();
this.header = this;
this.lastScrollY = 0;
this.scrollThreshold = 100;
}
connectedCallback() {
window.addEventListener('scroll', this.onScroll.bind(this), { passive: true });
}
onScroll() {
const currentScrollY = window.scrollY;
// Don't hide if near the top
if (currentScrollY < this.scrollThreshold) {
this.show();
return;
}
// Scrolling down
if (currentScrollY > this.lastScrollY) {
this.hide();
}
// Scrolling up
else {
this.show();
}
this.lastScrollY = currentScrollY;
}
hide() {
this.classList.add('is-hidden');
}
show() {
this.classList.remove('is-hidden');
}
}
customElements.define('sticky-header', StickyHeader);
.header {
position: sticky;
top: 0;
z-index: 100;
transition: transform 0.3s ease;
}
.header.is-hidden {
transform: translateY(-100%);
}

Compact Header on Scroll

Reduce header height when scrolling:

class StickyHeader extends HTMLElement {
connectedCallback() {
window.addEventListener(
'scroll',
() => {
if (window.scrollY > 50) {
this.classList.add('is-scrolled');
} else {
this.classList.remove('is-scrolled');
}
},
{ passive: true }
);
}
}
customElements.define('sticky-header', StickyHeader);
.header {
position: sticky;
top: 0;
z-index: 100;
padding: var(--spacing-md) 0;
transition:
padding 0.3s,
box-shadow 0.3s;
}
.header.is-scrolled {
padding: var(--spacing-sm) 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* Shrink logo */
.header__logo img {
transition: height 0.3s;
}
.header.is-scrolled .header__logo img {
height: 40px;
}

Announcement Bar Section

Basic Announcement

{# sections/announcement-bar.liquid #}
{%- if section.settings.text != blank -%}
<div class="announcement-bar" style="background: {{ section.settings.background }}; color: {{ section.settings.text_color }};">
<div class="announcement-bar__container container">
{%- if section.settings.link != blank -%}
<a href="{{ section.settings.link }}" class="announcement-bar__link">
{{ section.settings.text }}
</a>
{%- else -%}
<p class="announcement-bar__text">{{ section.settings.text }}</p>
{%- endif -%}
</div>
</div>
{%- endif -%}
{% schema %}
{
"name": "Announcement Bar",
"settings": [
{
"type": "text",
"id": "text",
"label": "Text",
"default": "Free shipping on orders over $50"
},
{
"type": "url",
"id": "link",
"label": "Link"
},
{
"type": "color",
"id": "background",
"label": "Background color",
"default": "#000000"
},
{
"type": "color",
"id": "text_color",
"label": "Text color",
"default": "#ffffff"
}
]
}
{% endschema %}

Multiple Announcements with Rotation

Use blocks for multiple messages:

{# sections/announcement-bar.liquid #}
{%- if section.blocks.size > 0 -%}
<announcement-bar
class="announcement-bar"
data-autoplay="{{ section.settings.autoplay }}"
data-delay="{{ section.settings.delay }}"
style="background: {{ section.settings.background }}; color: {{ section.settings.text_color }};"
>
<div class="announcement-bar__slider">
{%- for block in section.blocks -%}
<div
class="announcement-bar__slide {% if forloop.first %}is-active{% endif %}"
{{ block.shopify_attributes }}
>
{%- if block.settings.link != blank -%}
<a href="{{ block.settings.link }}" class="announcement-bar__link">
{{ block.settings.text }}
</a>
{%- else -%}
<span class="announcement-bar__text">{{ block.settings.text }}</span>
{%- endif -%}
</div>
{%- endfor -%}
</div>
{%- if section.blocks.size > 1 -%}
<div class="announcement-bar__nav">
<button class="announcement-bar__prev" aria-label="Previous announcement">
{% render 'icon-chevron-left' %}
</button>
<button class="announcement-bar__next" aria-label="Next announcement">
{% render 'icon-chevron-right' %}
</button>
</div>
{%- endif -%}
</announcement-bar>
{%- endif -%}
{% schema %}
{
"name": "Announcement Bar",
"settings": [
{
"type": "checkbox",
"id": "autoplay",
"label": "Auto-rotate announcements",
"default": true
},
{
"type": "range",
"id": "delay",
"label": "Rotation delay",
"min": 3,
"max": 10,
"step": 1,
"default": 5,
"unit": "s"
},
{
"type": "color",
"id": "background",
"label": "Background color",
"default": "#000000"
},
{
"type": "color",
"id": "text_color",
"label": "Text color",
"default": "#ffffff"
}
],
"blocks": [
{
"type": "announcement",
"name": "Announcement",
"settings": [
{
"type": "text",
"id": "text",
"label": "Text",
"default": "Announcement text"
},
{
"type": "url",
"id": "link",
"label": "Link"
}
]
}
],
"presets": [
{
"name": "Announcement Bar",
"blocks": [
{ "type": "announcement" }
]
}
]
}
{% endschema %}

Announcement Slider JavaScript

class AnnouncementBar extends HTMLElement {
constructor() {
super();
this.slides = this.querySelectorAll('.announcement-bar__slide');
this.currentIndex = 0;
this.autoplay = this.dataset.autoplay === 'true';
this.delay = parseInt(this.dataset.delay) * 1000 || 5000;
this.interval = null;
}
connectedCallback() {
if (this.slides.length <= 1) return;
this.querySelector('.announcement-bar__prev')?.addEventListener('click', () => this.prev());
this.querySelector('.announcement-bar__next')?.addEventListener('click', () => this.next());
if (this.autoplay) {
this.startAutoplay();
// Pause on hover
this.addEventListener('mouseenter', () => this.stopAutoplay());
this.addEventListener('mouseleave', () => this.startAutoplay());
}
}
startAutoplay() {
this.interval = setInterval(() => this.next(), this.delay);
}
stopAutoplay() {
clearInterval(this.interval);
}
next() {
this.goTo((this.currentIndex + 1) % this.slides.length);
}
prev() {
this.goTo((this.currentIndex - 1 + this.slides.length) % this.slides.length);
}
goTo(index) {
this.slides[this.currentIndex].classList.remove('is-active');
this.currentIndex = index;
this.slides[this.currentIndex].classList.add('is-active');
}
}
customElements.define('announcement-bar', AnnouncementBar);
.announcement-bar {
position: relative;
padding: var(--spacing-sm) var(--spacing-lg);
text-align: center;
font-size: 0.875rem;
}
.announcement-bar__slider {
position: relative;
overflow: hidden;
}
.announcement-bar__slide {
display: none;
}
.announcement-bar__slide.is-active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.announcement-bar__link {
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
}
.announcement-bar__nav {
position: absolute;
top: 50%;
left: var(--spacing-sm);
right: var(--spacing-sm);
transform: translateY(-50%);
display: flex;
justify-content: space-between;
pointer-events: none;
}
.announcement-bar__prev,
.announcement-bar__next {
pointer-events: auto;
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: var(--spacing-xs);
opacity: 0.7;
transition: opacity 0.2s;
}
.announcement-bar__prev:hover,
.announcement-bar__next:hover {
opacity: 1;
}

Dismissible Announcement

Let users close the announcement:

<dismissible-bar
class="announcement-bar"
id="announcement-{{ section.id }}"
data-storage-key="announcement-{{ section.id }}-dismissed"
>
<div class="announcement-bar__content">
{{ section.settings.text }}
</div>
<button class="announcement-bar__close" aria-label="Dismiss announcement">
{% render 'icon-close' %}
</button>
</dismissible-bar>
class DismissibleBar extends HTMLElement {
constructor() {
super();
this.storageKey = this.dataset.storageKey;
}
connectedCallback() {
// Check if already dismissed
if (localStorage.getItem(this.storageKey)) {
this.remove();
return;
}
this.querySelector('.announcement-bar__close')?.addEventListener('click', () => {
this.dismiss();
});
}
dismiss() {
// Store dismissal
localStorage.setItem(this.storageKey, 'true');
// Animate out
this.style.transition = 'all 0.3s';
this.style.maxHeight = this.offsetHeight + 'px';
requestAnimationFrame(() => {
this.style.maxHeight = '0';
this.style.padding = '0';
this.style.overflow = 'hidden';
});
setTimeout(() => this.remove(), 300);
}
}
customElements.define('dismissible-bar', DismissibleBar);

Handling Content Offset

When the header is sticky, content can jump under it. Handle this with CSS:

/* Using scroll-padding for anchor links */
html {
scroll-padding-top: var(--header-height, 80px);
}
/* Or with margin on main content */
main {
margin-top: var(--header-height, 80px);
}
/* Calculate header height dynamically */
.header {
--header-height: 80px;
}
.header.is-scrolled {
--header-height: 60px;
}

Dynamic Height with JavaScript

class StickyHeader extends HTMLElement {
connectedCallback() {
this.updateHeight();
window.addEventListener('resize', () => this.updateHeight());
}
updateHeight() {
const height = this.offsetHeight;
document.documentElement.style.setProperty('--header-height', `${height}px`);
}
}

Complete Header Group

Combine announcement and header in a section group:

{# sections/header-group.json #}
{
"type": "header",
"name": "Header Group",
"sections": {
"announcement": {
"type": "announcement-bar"
},
"header": {
"type": "header"
}
},
"order": ["announcement", "header"]
}
{# layout/theme.liquid #}
<body>
{% sections 'header-group' %}
<main>
{{ content_for_layout }}
</main>
{% sections 'footer-group' %}
</body>

Performance Considerations

Throttle Scroll Events

function throttle(func, wait) {
let waiting = false;
return function (...args) {
if (waiting) return;
func.apply(this, args);
waiting = true;
setTimeout(() => {
waiting = false;
}, wait);
};
}
window.addEventListener(
'scroll',
throttle(() => {
// Scroll handler
}, 100),
{ passive: true }
);

Use Passive Listeners

window.addEventListener('scroll', handler, { passive: true });

Avoid Layout Thrashing

// BAD: Causes layout thrashing
window.addEventListener('scroll', () => {
const height = header.offsetHeight; // Forces layout
header.style.top = `${height}px`; // Triggers layout
});
// GOOD: Batch reads and writes
let height;
window.addEventListener('scroll', () => {
// Read
height = header.offsetHeight;
// Write in next frame
requestAnimationFrame(() => {
header.style.top = `${height}px`;
});
});

Practice Exercise

Build a header system that:

  1. Shows announcement bar on first visit
  2. Allows dismissal with localStorage persistence
  3. Becomes sticky after announcement
  4. Shrinks on scroll
  5. Hides when scrolling down, shows when scrolling up

Test:

  • Does the announcement stay dismissed after refresh?
  • Is scroll performance smooth?
  • Do anchor links account for header height?

Key Takeaways

  1. CSS position: sticky handles basic sticky behavior
  2. Track scroll direction to show/hide on scroll
  3. Add visual feedback (shadow, compact mode) when scrolled
  4. Rotate announcements with blocks and JavaScript
  5. Use localStorage for dismissible announcements
  6. Set scroll-padding-top for anchor link offsets
  7. Throttle scroll handlers for performance
  8. Use passive listeners for scroll events

Module Complete!

Congratulations on completing Module 7! You now understand Shopify’s menu system, navigation Liquid objects, and how to build complete header and mobile navigation systems with sticky behavior and announcements.

Next up: Module 8: Global UI: Footer where you’ll build the complementary footer section.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...