Hide Header on Scroll

The header slides away while you scroll down to read and slides back the moment you scroll up to look for it.

  • JS
  • Layout
  • Scroll
  • Added
Field notes
Scroll down, then up

AI prompt

Create a header that hides on scroll down and reappears on scroll up, in React. Keep the last scroll position in a ref. On each scroll event, compare the new scrollTop: if it moved down by more than 4px and is past the header height, set a data-hidden attribute on the header; if it moved up by more than 4px, remove it. The header is position: sticky at the top and uses transform: translateY(-100%) when hidden, with a 280ms cubic-bezier(0.16, 1, 0.3, 1) transform transition. Use a small threshold so tiny trackpad jitters do not make it flicker. Always show the header when focus moves inside it. Under prefers-reduced-motion, hide and show without the slide. The brand reads "Field notes".

Source

Demo.tsx
'use client'

import { useRef } from 'react'
import s from './demo.module.css'

export default function HideHeader({ labels }: { labels: Record<string, string> }) {
  const header = useRef<HTMLElement>(null)
  const last = useRef(0)

  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const y = e.currentTarget.scrollTop
    const dy = y - last.current
    if (Math.abs(dy) < 4) return
    header.current?.toggleAttribute('data-hidden', dy > 0 && y > 48)
    last.current = y
  }

  return (
    <div className={s.frame} tabIndex={0} onScroll={onScroll}>
      <header ref={header} className={s.header}>
        <span className={s.logo} aria-hidden="true" />
        <strong className={s.brand}>{labels.brand}</strong>
        <span className={s.menu} aria-hidden="true" />
      </header>
      <span className={s.hint}>{labels.hint}</span>
      <div className={s.feed}>
        {Array.from({ length: 6 }, (_, i) => (
          <div key={i} className={s.post}>
            <span className={s.thumb} />
            <span className={s.lines}>
              <span />
              <span />
            </span>
          </div>
        ))}
      </div>
    </div>
  )
}
demo.module.css
.frame {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }

.hint {
  display: block;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--stage-muted);
}

.header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 48px;
  padding: 0 16px;
  border-bottom: 1px solid var(--stage-rule);
  background: var(--stage-surface);
  transition: transform 280ms var(--ease-out);
}

.header[data-hidden]:not(:focus-within) { transform: translateY(-100%); }

.logo { width: 20px; height: 20px; border-radius: 6px; background: var(--stage-accent); }
.brand { flex: 1; font: 600 var(--text-sm) / 1 var(--font-display); color: var(--stage-ink); }

.menu {
  width: 18px;
  height: 10px;
  border-block: 2px solid var(--stage-muted);
}

.frame > .hint { margin: 16px 0 4px; }

.feed {
  display: grid;
  gap: 10px;
  max-width: 300px;
  margin: 0 auto;
  padding: 8px 16px 24px;
}

.post {
  display: flex;
  gap: 12px;
  padding: 10px;
  border: 1px solid var(--stage-rule);
  border-radius: 12px;
}

.thumb { flex: none; width: 44px; height: 44px; border-radius: 8px; background: var(--stage-surface); }

.lines { display: grid; flex: 1; align-content: center; gap: 8px; }
.lines span { height: 8px; border-radius: 4px; background: var(--stage-surface); }
.lines span:last-child { width: 60%; }

@media (prefers-reduced-motion: reduce) {
  .header { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.