Communitygithub.com

brainzcode/premium-landing

Add Lenis smooth scrolling to a site correctly — setup, anchor offsets for sticky headers, scroll locking for modals and menus, reduced-motion handling. Use when asked for smooth scroll, Lenis, momentum/eased scrolling, or when smooth scroll broke a sticky header, a modal lock, or anchor links. Includes the sticky-position trap that only appears at non-zero scroll.

premium-landing とは?

premium-landing is a Claude Code agent skill that add Lenis smooth scrolling to a site correctly — setup, anchor offsets for sticky headers, scroll locking for modals and menus, reduced-motion handling. Use when asked for smooth scroll, Lenis, momentum/eased scrolling, or when smooth scroll broke a sticky header, a modal lock, or anchor links. Includes the sticky-position trap that only appears at non-zero scroll.

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/brainzcode/premium-landing/tree/HEAD/plugins/premium-landing/skills/lenis-smooth-scroll

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Lenis smooth scroll

Lenis intercepts wheel input and eases the native scroll position. Sticky positioning, IntersectionObserver, and window.scrollY all keep working — which is why it's the right choice. The failures come from things that fight it, not from Lenis itself.

Setup

Vendor it rather than hot-linking a CDN, so the site stays self-contained and works from file://:

curl -sSL -o assets/vendor/lenis.min.js https://unpkg.com/[email protected]/dist/lenis.min.js

The dist/lenis.min.js build is an IIFE exposing globalThis.Lenis — fine in a plain <script>. (dist/lenis.mjs is ESM and will not work in a plain script tag.) Load it before your own script.

var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
var lenis = null;

if (!reduced && window.Lenis) {
  var navH = parseInt(getComputedStyle(document.documentElement)
                        .getPropertyValue('--nav-h'), 10) || 0;
  lenis = new Lenis({
    duration: 1.05,
    easing: t => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
    smoothWheel: true,
    syncTouch: false,        // leave touch native — smoothing it feels laggy on phones
    autoRaf: true,           // Lenis runs its own RAF loop; no manual ticker needed
    anchors: { offset: -(navH + 10) }   // in-page links clear the sticky header
  });
} else {
  // no Lenis — give the browser its own behaviour back
  document.documentElement.style.scrollBehavior = reduced ? 'auto' : 'smooth';
}

Required CSS

html { /* NO scroll-behavior: smooth — it fights Lenis */ }

html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }

scroll-behavior: smooth on html must go. If you keep it, anchor jumps double-animate and fight the easing.

Trap 1 — overflow: clip kills position: sticky

This is the one that will cost you an hour.

lenis.stop() adds .lenis-stopped, whose CSS sets overflow: clip on <html>. That removes the scrollport, and position: sticky has nothing left to stick to — sticky elements snap back to their position in the document, i.e. the top of the page.

Open a menu at scroll 1200 and your sticky header renders at top: -1200. Off screen. Along with its close button. Users report it as "the close button disappeared."

The same thing happens with the classic body { position: fixed } scroll lock, for the same reason — no scrollport.

Fix: pin the header to the viewport for as long as scrolling is locked.

html.menu-open .nav-wrap { position: fixed; top: 0; left: 0; right: 0; }
document.documentElement.classList.toggle('menu-open', open);

Swap the class while the overlay is still opaque so the sticky→fixed layout change is invisible. Apply it on both the Lenis and the reduced-motion paths.

Test overlays at a non-zero scroll offset. At scroll 0 a broken sticky header is pixel-identical to a working one, so the bug hides completely. Verify at 0, ~1200, and deep in the page.

Trap 2 — don't stack two scroll locks

Pinning the body and calling lenis.stop() makes them fight: position: fixed on body collapses the page height, Lenis resyncs to 0, and closing the overlay dumps the user at the top of the page.

With Lenis, stop() plus its own overflow: clip holds the page without touching the body — so the scroll position is never lost and there is nothing to restore:

if (lenis) {
  open ? lenis.stop() : lenis.start();
} else if (open) {                       // reduced-motion path only
  scrollY = window.scrollY;
  document.body.style.position = 'fixed';
  document.body.style.top = -scrollY + 'px';
  document.body.style.width = '100%';
} else if (document.body.style.position === 'fixed') {
  document.body.style.position = '';
  document.body.style.top = '';
  document.body.style.width = '';
  window.scrollTo(0, scrollY);
}

Verify: scroll to 1200 → open → wheel → close. Position must read 1200 at every step, and scrolling must work again afterwards.

Trap 3 — anchor links

Lenis owns scrolling, so raw href="#id" jumps look wrong. Use the built-in anchors option with a negative offset equal to your sticky header height, or route clicks manually through lenis.scrollTo(target, { offset: -navH }).

Check the landing position: the section top must sit at or below the header's bottom edge, not underneath it.

Trap 4 — Lenis is usually not your jank

When scroll feels bad after adding Lenis, measure before blaming it. In one build native scroll actually measured worse (13% dropped frames vs 9%) — Lenis was fine and the real costs were elsewhere:

  • a fixed full-viewport mix-blend-mode overlay (film grain): 34% → 3% dropped frames just by removing the blend mode
  • backdrop-filter on a sticky element, which is on screen for every frame of every scroll
  • backdrop-filter on large elements generally — it re-samples per frame

Frame-timing harness

// in page
window.__f = []; let l = performance.now();
const t = n => { window.__f.push(n - l); l = n; requestAnimationFrame(t); };
requestAnimationFrame(t);

Drive it with real wheel events over the suspect section, then report mean, p95, and the percentage of frames > 33ms. Bisect by disabling one effect at a time with add_style_tag. Healthy is ~16.7ms mean and 0% long frames; anything over ~10% long frames is visible stutter.

Checklist

  • scroll-behavior: smooth removed from html
  • Lenis CSS block added
  • autoRaf: true (or a manual RAF loop — not both)
  • anchors offset matches the sticky header height
  • syncTouch: false so phones keep native momentum
  • Sticky headers pinned while any overlay locks scrolling
  • Only one scroll-lock mechanism in play
  • Overlays tested at a non-zero scroll offset
  • prefers-reduced-motion skips Lenis and restores native behaviour
  • Scroll position preserved across open → close
  • Frame timing measured, not assumed

Individual skills in this repo

This repo contains 7 individual skills — each has its own dedicated page.

brainzcode/premium-landing

Add a studio or agency build credit to a website footer — "Site by <Studio>" linking to the studio's site. Use when finishing or shipping a site, adding a footer, or when asked for an agency credit, build credit, designed-by line, or attribution. Covers wording, placement opposite the client's copyright, markup for each stack, styling from the host site's tokens, and the sitewide-footer-link SEO footprint when the same credit runs across many client sites.

brainzcode/premium-landing

Build a complete premium landing page, either replicated from a reference screenshot/URL or designed from a written brief, in HTML, React, Next.js or Astro. Use when asked to build, design, replicate or clone a landing page, marketing page, or one-page site; when handed a design screenshot or Figma/Pinterest reference to turn into code; or when a landing page needs a full premium pass. Covers design tokens, section patterns, the layout traps that silently break grids and marquees, and screenshot-based responsive verification from 320px to 1920px.

brainzcode/premium-landing

Build true Apple-style liquid glass in the browser — real refraction via SVG displacement maps, chromatic dispersion, specular edges. Use when asked for liquid glass, glassmorphism, frosted/glass cards, glass navbars, or when a blur-based "glass" effect looks flat. Includes the performance trap that makes backdrop-filter stutter on scroll and the contrast trap that makes text unreadable on glass.

brainzcode/premium-landing

Build a premium mobile nav — a hamburger button that morphs into a close X, a full-screen overlay panel with staggered link reveal, scroll lock, focus trap, and reduced-motion handling. Use when asked for a mobile menu, hamburger/burger button, nav drawer, off-canvas menu, or when a menu loses scroll position, still scrolls behind on iOS, leaks Tab focus to the page underneath, or the close button stops responding. Includes the z-index trap that swallows the close button and the iOS rubber-band trap.

brainzcode/premium-landing

Rebuild a design accurately from a supplied reference — a screenshot, PDF page, mockup or live URL — by measuring it numerically instead of eyeballing it. Use when asked to replicate, clone, match or rebuild a design "exactly", "1 to 1" or "with no mistakes", or when a build drifts from its reference. Covers extracting the true content box from a padded screenshot, deriving element positions as percentages, solving object-fit framing, and knowing when a reference cannot be matched.

brainzcode/premium-landing

Ship link previews and favicons that actually work - Open Graph and Twitter card tags, a correctly framed 1200x630 share image, and a full favicon set (SVG, ICO, apple-touch, maskable manifest icons). Use when asked for social sharing, OG image, open graph, twitter card, link preview, share image, favicon, app icon, apple touch icon, or web manifest. Also use when a shared link previews blank, shows the wrong image, or a favicon will not appear. Includes the absolute-URL trap that silently blanks every preview and the font trap that breaks SVG favicons on other machines.

brainzcode/premium-landing

Source free, licence-clear photography for a site from Unsplash — searching by mood rather than noun, sizing and cropping for the slot, honouring the API's hotlink and download-tracking requirements, and writing real alt text. Use when a page needs a hero image, section photography, avatars or card art and none was supplied; when placeholder greys need replacing with real imagery; or when asked to find, pick or swap photos for a design.

関連スキル