דילוג לתוכן הראשי
Runs on every page

Engine

כל שינוי כאן ישפיע על האתר כולו.A plain Next.js install, and only what we did differently on top of it. How Next itself works is in its own docs. What runs on every page load and every route change without a line in the page: the boot order, the click rules, the current-page mark, route popups, the focus ring, a demo's body class (UkBodyClass) and Lenis. Read from es-scripts.

עדכון אחרון: 7.11.2026

#add-code

Add code to every page

Code that runs on every page goes in one of the files below. Do not add a new component to app/layout.tsx for it: that way everything that runs at startup, and its order, stays in one place.

// es-scripts/_init/uk_global_init.tsx

// once, on the first paint: in the "Mount-once setup" useEffect
console.log('hello')

// on every route change: in the useEffect that depends on [pathname]
console.log('hello', pathname)

// an animation or a slider: es-scripts/uk-global-scripts.ts, initGlobalAnimations()
// one import + one try { my_init() } catch {} line. The order matters

// a third-party tag (GA, a pixel): components/studio/SiteTags.tsx

#one-or-all

One page or the whole site

Page settings and site settings, in Webflow words. A library imported by a component is sent only to the pages that use that component, so one page costs the others nothing.

One page (page settings)

// components/uikit/UkGallery/UkGallery.tsx
'use client'
export default function UkGallery({ children }) {
  const box = useRef(null)
  useEffect(() => {
    let grid
    // on the client only, when the page opens
    import('masonry-layout').then(({ default: Masonry }) => {
      grid = new Masonry(box.current, { itemSelector: 'figure' })
    })
    return () => grid?.destroy()
  }, [])
  return <div ref={box}>{children}</div>
}

// app/gallery/page.tsx: the only page that gets it
<UkGallery>…</UkGallery>

The real example: components/fancybox/carousel_autoscroll/CarouselAutoscroll.tsx.

The whole site (site settings)

// es-scripts/masonry_init.ts
export async function masonry_init() {
  const box = document.querySelector('[data-masonry]')
  if (!box) return   // a page without it: nothing, and nothing loads
  const { default: Masonry } = await import('masonry-layout')
  new Masonry(box, { itemSelector: 'figure' })
}

// es-scripts/uk-global-scripts.ts
import { masonry_init } from './masonry_init'

// in initGlobalAnimations()
try { masonry_init() } catch {}

It runs on every page and every route change, so it needs a matching destroy in destroyGlobalAnimations(). The check at its top is what keeps the cost off other pages: a library imported at the top of the file would go to every page, used or not.

#global-init

UkGlobalInit

<Component />

es-scripts/_init/uk_global_init.tsx: a client component that renders nothing, mounted once in app/layout.tsx, so it runs on every page, the studio and every demo. It is the one place for code that runs on every load and every route change (CLAUDE.md 1.2).

Once, on the first paint

  • setupResizeListener(): 250ms after the last resize, every ScrollTrigger is sorted and refreshed.
  • fancybox_init(): binds [data-fancybox] for the whole site, in the page's direction (RTL), with the Lenis hand-off (see Lenis) and the Back handling (popup_history.ts, see Route popup). It also sets the Hebrew labels, a counter that reads correctly in RTL, no fullscreen button, no drag on HTML popups, and names the dialog from its bar title or first heading. Fancybox's CSS loads after the page is up, off the first paint.Fancybox
  • focus_quiet_init(): the focus ring rule (Focus quiet).
  • One ScrollTrigger.refresh() on window load: late content (a carousel that injects its slides) moves sections after the first measure.
  • The popstate listener, and a click listener on document in the capture phase (Clicks and Back).
  • window.__ukNavigate, the router for plain scripts.

On every route change

At once on every change: ?lines draws a 24px rule over the page and ?lines=0 takes it off (uk-lines-view, data-lines-view). ?seo turns the SEO view on and ?seo=0 off (uk-seo-view in sessionStorage, data-seo-view on <html>, the volume badge of DevVolume), and it stays on from page to page. Then, when the pathname changes (and on the first load), 50ms later:

  1. initGlobalAnimations(), in the order below.
  2. update_active_links(): the .is-current mark.
  3. gsap:init on window.
  4. <html data-uk-ready>, even if a step threw.
  5. A forward navigation scrolls to the top, through Lenis when it is on (scrollTo(0, { immediate: true })). Back and Forward do not: the browser puts the reader back by itself.

A navigation is a change of path, compared with the last one, not a first-mount flag: React runs the effect twice in dev, and a flag took every reload for a navigation. A ?query or #hash change keeps the path: no new init.

<html data-uk-ready>
…
window.__ukCameFrom
…
Read live from this page. Follow a link to another docs page and come back with a link: __ukCameFrom names the page you came from.

Code: rebuild on gsap:init

'use client'
import { useEffect } from 'react'

// a component that builds its own ScrollTrigger: the global init kills
// every trigger on each route change, then fires gsap:init
useEffect(() => {
  const build = () => { /* gsap.to(el, { scrollTrigger: { … } }) */ }
  window.addEventListener('gsap:init', build)
  build()
  return () => window.removeEventListener('gsap:init', build)
}, [])

What it gives the page

  • gsap:initwindow event
    Fired after every init, once the page is set. Listened to by every component that builds its own ScrollTrigger, by UkPageLoader (it pauses Lenis again) and by UkPageTransitions (its arrival).
  • data-uk-readyattribute on <html>
    Set after the first init, in a finally, so a step that threw does not hold it back. Until then [trigger_box] elements are still hidden. UkLiveFrame waits for it before it lifts its UkLoader.
  • window.__ukNavigate(href: string) => void
    The Next router (router.push) for plain scripts: lib/follow-link.ts (the hand-built slides of SyncSlidersClassic and SyncWithVerticalText) and UkPageTransitions navigate client side through it.
  • window.__ukCameFromstring
    The path a client-side navigation came from. UkLiveFrame's X goes back there.

No props. One instance, in app/layout.tsx.

Rules

  • Code that must run on every page or route change goes inside uk_global_init.tsx. Never a parallel <MyInit /> mounted in app/layout.tsx (CLAUDE.md 1.2).
  • An animation or slider init goes into initGlobalAnimations() in es-scripts/uk-global-scripts.ts: one import and one try { … } catch {} line.
  • A component that builds its own ScrollTrigger builds on mount AND on gsap:init: the init kills every trigger after the page's own effects ran.

Live in

#init-order

initGlobalAnimations: the order

What every route change runs, as es-scripts/uk-global-scripts.ts calls it. This list is read from that file when the page is built (app/docs/global/_init-order.ts), so it cannot drift. Every script skips itself when its DOM marker is not on the page, and each script call sits in its own try: one that throws does not stop the rest (the teardown and the two refreshes are not wrapped).

  • The numbers are only the order in the file. Only the steps below are in that place for a reason. Inside a group (the scroll animations, the Swipers) the order means nothing: each step looks for its own attribute and does nothing on a page without it.
  • destroyGlobalAnimations() first: kills every ScrollTrigger and its scroll memory, stops video autoplay and the mouse-move effect, and clears the body theme (its observer and the uk-body-* classes on <body>). Not Lenis: one instance lives across routes.
  • lenis_init() right after, before any ScrollTrigger is built: the triggers follow Lenis's scroll (lenis.on('scroll', ScrollTrigger.update)). Don't reorder.
  • refreshScrollTriggers(): ScrollTrigger.sort() + refresh(), then .gsap-ready on every .opacity_0_on_load. Once between the animations and the sliders, once more on the next frame, for layout that moved during the init.
  1. 1destroyGlobalAnimationsGSAP
  2. 2lenis_initGSAPes-scripts/LENIS/uk-lenis-init.js
  3. 3trigger_box_animationGSAPes-scripts/GSAP/trigger_box_animation.js
  4. 4split_text_animation_initGSAPes-scripts/GSAP/split_text_animation.js
  5. 5countUpGSAPes-scripts/GSAP/count_up_animation.js
  6. 6slot_counter_animationGSAPes-scripts/GSAP/slot_counter_animation.js
  7. 7clip_reveal_animationGSAPcomponents/GSAP/ClipReveal/clip_reveal_animation.js
  8. 8parallax_animationGSAPes-scripts/GSAP/parallax_animation.js
  9. 9section_clip_animationGSAPes-scripts/GSAP/section_clip_animation.js
  10. 10fit_to_target_animationGSAPes-scripts/GSAP/fit_to_target_animation.js
  11. 11movement_animationGSAPes-scripts/GSAP/movement_animation.js
  12. 12rotate_x_stack_animationGSAPes-scripts/GSAP/rotate_x_stack_animation.js
  13. 13shade_overlay_animationGSAPes-scripts/GSAP/shade_overlay_animation.js
  14. 14mouse_move_on_elementGSAPes-scripts/GSAP/mouse_move_on_element.js
  15. 15cover_hero_animationGSAPes-scripts/GSAP/cover_hero_animation.js
  16. 16blur_bottom_animationGSAPes-scripts/GSAP/blur_bottom_animation.js
  17. 17cta_sticky_animationGSAPes-scripts/GSAP/cta_sticky_animation.js
  18. 18scrolling_text_animationGSAPes-scripts/GSAP/scrolling_text_animation.js
  19. 19video_autoplay_animationGSAPes-scripts/GSAP/video_autoplay_animation.js
  20. 20body_themeGSAPes-scripts/GSAP/body_theme.js
  21. 21ground_fade_animationGSAPes-scripts/GSAP/ground_fade_animation.js
  22. 22zoom_out_nine_photos_grid_animationGSAPcomponents/GSAP/ZoomOutNinePhotosGrid/zoom_out_nine_photos_grid_animation.js
  23. 23grid_assemble_animationGSAPcomponents/GSAP/GridAssemble/grid_assemble_animation.js
  24. 24puzzle_on_scrollGSAPes-scripts/GSAP/puzzle_on_scroll.js
  25. 25refreshScrollTriggersGSAP
  26. 26scale_active_swiper_initSwipercomponents/Swiper/creative_scale_active/creative_scale_active_swiper
  27. 27classic_grid_swiper_initSwipercomponents/Swiper/classic_grid/classic_grid_swiper
  28. 28swiper_tabs_slider_initSwipercomponents/Swiper/swiper_tabs_slider/swiper_tabs_slider_swiper
  29. 29sync_slider_initSwipercomponents/Swiper/sync_sliders_classic/sync_sliders_classic_swiper
  30. 30sync_with_vertical_text_initSwipercomponents/Swiper/sync_with_vertical_text/sync_with_vertical_text_swiper
  31. 31swiper_sub_menu_initSwipercomponents/Swiper/swiper_sub_menu/swiper_sub_menu
  32. 32fashion_slider_initSwipercomponents/Swiper/ui-initiative/fashion_slider/fashion-slider-swiper
  33. 33super_flow_swiper_initSwipercomponents/Swiper/ui-initiative/super_flow/super-flow-swiper
  34. 34material_you_swiper_initSwipercomponents/Swiper/ui-initiative/material_you_slider/material-you-swiper
  35. 35expo_slider_initSwipercomponents/Swiper/ui-initiative/expo_slider/expo-slider-swiper
  36. 36shaders_slider_initSwipercomponents/Swiper/ui-initiative/shaders_slider/shaders-slider-swiper
  37. 37trigger_slide_animationGSAPes-scripts/GSAP/trigger_slide_animation.js
  38. 38refreshScrollTriggersGSAP · next frame

#click-rules

Clicks and Back

One click listener on document, in the capture phase, so it runs before any <Link> or component handler. It reads the clicked <a>'s own href attribute and applies the first rule that fits:

  1. Left to the browser: no href, one that starts with http, mailto: or tel:, an in-page #anchor, target="_blank" or download.
  2. A route popup: a plain left click (no Cmd, Ctrl, Shift, Alt) on a path that opens as a popup on this page, unless the link carries data-no-popup: that one goes to the page itself (the "צור קשר" of the studio footer and mobile menu). preventDefault + stopPropagation, and the popup opens (Route popup). Other capture listeners on document (SiteTags' conversion tags) still hear it; a bubbling one does not.
  3. A demo from the studio: the href starts with /demos/ and this page is not under /demos. window.open(href, '_blank', 'noopener'); a modified click is the browser's. Inside a demo its own links stay in the tab. ESLint's uk/demo-link-new-tab asks for target="_blank" where it can read the href; this covers hrefs from data.
  4. The same page: no # in the href, a plain click, and both its path and its ?query are the current ones. A link to the same path with another query is another page (another home of a demo), not this one. Straight to the top (Lenis immediate). A <Link> to the page you are on does nothing in Next; this is the "click the page you are on, go up" of a normal site, for <Link> and plain <a> alike.
  5. A link to the path you are on that rule 4 did not take (another ?query, a #hash, a modified click): nothing, the page stays mounted and no trigger is killed. Any other internal link: destroyGlobalAnimations() before Next navigates, so the old page's triggers go with it.

Back and Forward (popstate): popup_history_back() (es-scripts/_init/popup_history.ts) first. If the entry was a popup's (any Fancybox popup), the popup closes and that is all: same page. Otherwise the next route change is marked as Back (no scroll to the top, the saved offset comes back) and destroyGlobalAnimations() runs before the page swaps.

Known gap (17.10): rule 5 also takes a Cmd or Ctrl click on a link to another path (the page stays, the link opens in a new tab). The triggers are killed and, since the path does not change, nothing rebuilds them until the next navigation: a [data-movement] on /docs/gsap/scroll-depth stops moving. Back or Forward between two #hash entries of one page (the hero's "On this page" pills) does the same: the popstate handler kills the triggers and the path does not change.

  • This pagerule 4 · same path: back to the top
  • /contactrule 2 · a route popup: the contact popup opens
  • /demos/yogarule 3 · a demo from the studio: a new tab
  • /docs/navbarrule 5 · any other page: triggers killed, Next navigates
Real links, hrefs from a data array. Each one takes the rule beside it.

Code: which rule a link gets

<Link href="/about">אודות</Link>                    // rule 5 (rule 4 on /about itself)
<Link href={demo.url}>{demo.title}</Link>          // from data: rule 3, a new tab
<Link href="/demos/yoga" target="_blank" rel="noopener">יוגה</Link>   // rule 1
<a href="https://wa.me/…" target="_blank" rel="noopener noreferrer">וואטסאפ</a>   // rule 1

Rules

  • Internal links are <Link> (CLAUDE.md 1.4): on a plain <a href="/…"> the rules still run, then the browser reloads the page.
  • A component that navigates on its own (a slide built by hand) goes through followLink() (lib/follow-link.ts), which calls window.__ukNavigate, not window.location.
  • A demo link written out in the studio carries target="_blank" rel="noopener" (ESLint uk/demo-link-new-tab); rule 3 is the net for hrefs that come from data.

Live in

Every <a href> that leads to the page you are on carries the class is-current: the navbar, the mobile menu, the footer, a sidebar, a link in the copy. update_active_links() in es-scripts/_init/active_link_marker.js puts it on and takes it off, on every route change. No component computes it.

When to use

Any "you are here" look outside the navbar's desktop items: style .is-current in that scope. The navbar's own items also carry aria-current="page" from UkNavbar, with a prefix match the class does not have; both are styled together (/docs/navbar#nav-current).

Real links. This page styles a.is-current in its own CSS so the mark shows.

Code

/* any scope: a footer, a sidebar, the copy */
.my-footer a.is-current { color: var(--primary); }

/* uk-navbar-colors.css: the navbar's own, both marks together */
.uk-nav-item[aria-current="page"],
.uk-nav-item.is-current,
.uk-mobile-nav a.is-current:not(.uk-button) { … }

The two marks

  • .is-currentclass on <a>
    Set by update_active_links() (es-scripts/_init/active_link_marker.js) on every a[href] whose pathname equals this one, trailing slash ignored. Skips #…, mailto:, tel: and another origin.
  • aria-current="page"attribute
    Set by UkNavbar on its desktop items only. Prefix match (/services on /services/x), exact when a sibling item sits under it; an item without href is current when a child matches. activeFor adds paths outside its folder, inactiveFor removes paths. A plain dropdown child gets it on an exact match.

Rules

  • Style the current page through .is-current in your scope; never compare the pathname in a component.
  • The class is visual only. A menu of your own that needs aria-current for a screen reader sets it itself (WebflowTabs and WebinyCourseSidebar do).
  • It runs once per route change: a link that mounts afterwards is not marked until the next one.

Live in

#route-popup

Route popup: data-popup-for

[attribute]
Fancybox

A route that opens as a popup: a click on a link to /contact opens the contact popup over the page instead of leaving it, and /contact stays a real page for Google, a shared link and a new tab. es-scripts/_init/route_popup.ts, called by the click handler (rule 2).

  • The popup declares its route: a hidden Fancybox trigger with data-popup-for="/contact". A plain click on an internal link to that path, on a page that has the trigger and is not that path, with no #hash and no data-no-popup, clicks the trigger instead.
  • An open menu closes (uk-navbar:close). A link clicked in #uk-mobile-nav moves focus to the menu button first, so the dialog gives it back there on close.
  • Like every Fancybox popup, it gets a history entry of its own (es-scripts/_init/popup_history.ts): the same URL, { ...history.state, ukPopup: true }. Back closes it with no page change under it. Closed any other way (X, Esc, the backdrop), the entry is taken off with history.back(), and that popstate is ignored too.
The real ContactPopup: it is mounted on every studio page, this one included. Open it, then press Back: it closes and the page stays. Cmd or Ctrl and click opens /contact itself.

Code: another route as a popup

import FancyboxHtml from '@/components/fancybox/FancyboxHtml/FancyboxHtml'

export default function PricingPopup() {
  return (
    <>
      {/* the route's trigger: route_popup.ts clicks it */}
      <a href="#pricing-popup" data-fancybox="" data-popup-for="/pricing"
         hidden aria-hidden="true" tabIndex={-1} />
      <FancyboxHtml id="pricing-popup" title="מחירים">…</FancyboxHtml>
    </>
  )
}

Attributes

  • data-popup-forstring (a pathname)required
    On the hidden [data-fancybox] trigger: the path whose links open this popup. Matched exactly against the clicked link's pathname.
  • href"#<popup id>"required
    The trigger points at the FancyboxHtml id, like any Fancybox trigger.
  • data-no-popupboolean attributedefault off
    On a link: it goes to the page, not the popup.

Rules

  • One data-popup-for per path on a page: route_popup.ts clicks the first match only (document.querySelector). Keep the trigger's data-fancybox empty: an empty value opens only its own target.
  • Mount the popup where its links are. ContactPopup sits in components/studio/Layout.tsx, which renders a demo's children as they are: a demo never gets it, and its contact links go to its own page.
  • Keep the route a page. The popup is how a click reaches it, not a replacement for it.

Live in

  • Any studio page · the "צור קשר" in the header; the footer one goes to the page
  • /contact · the page itself: on its own path no popup opens

#focus-quiet

Focus quiet: data-focus-quiet

[attribute]

Every dialog on the site (Fancybox, the Radix menus, the popups) gives focus back to the element that opened it: a keyboard user has to land where they were. The browser draws the ring (:focus-visible) from the last input, and after Esc that was a key, so a mouse user who opened a gallery and closed it with Esc got a ring round the gallery. es-scripts/_init/focus_quiet.ts, mounted once:

  • Opened from the keyboard (Enter or Space): the ring shows, as the browser decides.
  • Opened with the pointer and closed with Esc: the element that gets focus in the next 1500ms (ESC_WINDOW) is marked [data-focus-quiet], and [data-focus-quiet][data-focus-quiet]:focus-visible { outline: none } hides it.
  • The first Tab clears every mark: from there it is keyboard navigation and the ring is back.
focus
…
:focus-visible
…
[data-focus-quiet]
…
ring
…
A real Fancybox popup. Click the button, press Esc: focus is back on it and the ring is hidden. Press Tab: the mark clears. Reach the button with Tab, press Enter, then Esc: the ring is drawn.

Attributes

  • data-focus-quietattribute (set by the script)
    On the element focus came back to, after a pointer-opened dialog closed with Esc. Hides its :focus-visible outline. Cleared by the first Tab, and when focus leaves it.

Rules

  • Nothing to add: it covers every dialog. Never write data-focus-quiet by hand.
  • Draw a focus ring as an outline on :focus-visible. The quiet rule removes the outline only: a ring drawn with box-shadow would still show after Esc.

Live in

#uk-body-class

UkBodyClass

<Component />

A demo's classes on <body> while the demo is open. Radix (the navbar's dropdowns and drawer), Fancybox and the cursor dot render onto <body>, outside the [data-demo] scope, so they read the brand's font and tokens from body.demo-<brand> in its CSS (CLAUDE.md §11). The classes go on when the layout mounts and come off when it unmounts, so nothing outlives the demo. Engine, not a piece: it shows nothing, it makes the demo's look reach what is rendered outside it.

Code

import UkBodyClass from '@/app/demos/_shared/UkBodyClass'

// app/demos/yoga/layout.tsx: next to data-demo
<div data-demo="yoga">
  <UkBodyClass classes="demo-yoga" />
  <YogaNavbar />
  {children}
  <Footer />
</div>

// a variant's skin rides in the same string (catania, v/[variant]/layout.tsx)
<UkBodyClass classes={`demo-catania demo-catania--${variant.slug}`} />

/* yoga.css, its two body.demo-yoga blocks (abridged): what the portalled UI reads, outside [data-demo] */
[data-demo="yoga"],
body.demo-yoga {
  --font-sans: 'Noto Sans Hebrew', system-ui, sans-serif;
  --font-display: var(--font-sans);
}
…
body.demo-yoga {
  font-family: var(--font-sans);
  --popover: #f7f1e6;
  …
}

Props

  • classesstringrequired
    One or more classes, space-separated: demo-<brand> first, then a variant's own. Added to <body> on mount, taken off on unmount and before a new string replaces them.

Rules

  • Once per demo, next to data-demo in the demo's layout.tsx when every home wears the same class. When a home adds its own class, put it in each home's chrome instead: (base)/layout.tsx + v/[variant]/layout.tsx (blog, catania, dentist) or the shared ConfChrome / SaasChrome. Never in a page.
  • A variant's class goes in the same string, not in a second UkBodyClass.
  • Only what a portal needs goes on body.demo-<brand> (the font, the popover and ring tokens); the rest stays in the [data-demo] block. Never :root: it would outlive the demo.

Live in

  • Yoga · demo-yoga, in the demo's layout.tsx (the usual shape)
  • Catania, the sushi home · demo-catania demo-catania--sushi: the restaurant's skin on its dropdowns
  • Conference, tourism · demo-conference demo-conference--tourism demo-conference--light, from ConfChrome

#lenis-scroll

Lenis

Lenis smooths the wheel on every page: lenis_init() in es-scripts/LENIS/uk-lenis-init.js, the first step of every init after the teardown. One instance (window.lenis), kept across route changes. Options: duration: 0.8, wheelMultiplier: 1, touchMultiplier: 2, an exponential ease, infinite: false. Its scroll drives ScrollTrigger, and GSAP's ticker drives Lenis (lagSmoothing(0)).

smoothWheel follows the visitor: under prefers-reduced-motion the wheel scrolls natively with no glide, and Lenis still runs for ScrollTrigger and lenis.scrollTo. The Lenis 1.3 defaults it keeps (syncTouch: false, allowNestedScroll: false) mean two things: touch scrolling on a phone stays native, and a box in the page with its own scroll does not get the mouse wheel. Lenis takes it and scrolls the page, unless the box says otherwise.

Native scroll where it is needed

  • A box in the page scrolls on its own (a wide table, a list, a log)
    data-lenis-prevent on the element that scrolls
    Lenis lets wheel and touch over it through to the browser. Narrower: data-lenis-prevent-wheel, -touch, -vertical, -horizontal.
  • A whole page needs native scroll (a sidebar with its own scroll)
    <UkLenisOff /> anywhere in the page
    lenis_destroy() on mount, lenis_init() on unmount. It renders <span data-lenis-off hidden>, and while that is in the DOM nothing recreates Lenis: not the route init, not Fancybox on close.
  • A Fancybox popup
    Nothing
    fancybox_init fires lenis:destroy when a popup opens and lenis:init when the last one closes (a nested popup closing leaves it off). The popup scrolls natively.
  • An overlay of your own (not Fancybox) with its own scroll
    lenis_destroy() on open, lenis_init() on close
    What Fancybox does. Or keep Lenis and put data-lenis-prevent on the overlay's scroll area.
  • Freeze the page under something
    lenis_pause() / lenis_resume()
    lenis.stop() / start(): every wheel and touch event is cancelled, an overlay's too, except under data-lenis-prevent. The instance stays.
  • Compare smooth and native while debugging a pin
    <UkLenisToggle />
    A debug button: lenis_destroy() / lenis_init() on click. Not for a client site.
window.lenis
…
no attribute
שורה 1 בתיבה עם גלילה משלה
שורה 2 בתיבה עם גלילה משלה
שורה 3 בתיבה עם גלילה משלה
שורה 4 בתיבה עם גלילה משלה
שורה 5 בתיבה עם גלילה משלה
שורה 6 בתיבה עם גלילה משלה
שורה 7 בתיבה עם גלילה משלה
שורה 8 בתיבה עם גלילה משלה
שורה 9 בתיבה עם גלילה משלה
שורה 10 בתיבה עם גלילה משלה
שורה 11 בתיבה עם גלילה משלה
שורה 12 בתיבה עם גלילה משלה
שורה 13 בתיבה עם גלילה משלה
שורה 14 בתיבה עם גלילה משלה
data-lenis-prevent
שורה 1 בתיבה עם גלילה משלה
שורה 2 בתיבה עם גלילה משלה
שורה 3 בתיבה עם גלילה משלה
שורה 4 בתיבה עם גלילה משלה
שורה 5 בתיבה עם גלילה משלה
שורה 6 בתיבה עם גלילה משלה
שורה 7 בתיבה עם גלילה משלה
שורה 8 בתיבה עם גלילה משלה
שורה 9 בתיבה עם גלילה משלה
שורה 10 בתיבה עם גלילה משלה
שורה 11 בתיבה עם גלילה משלה
שורה 12 בתיבה עם גלילה משלה
שורה 13 בתיבה עם גלילה משלה
שורה 14 בתיבה עם גלילה משלה
With a mouse wheel: over the box without the attribute the page scrolls, over the box with it the box scrolls. Pause Lenis: the page stops, the second box still scrolls. Turn Lenis off with the debug toggle: both scroll natively. On a phone both scroll anyway: touch is native.

Code

{/* a box with its own scroll */}
<div className="max-h-80 overflow-y-auto" data-lenis-prevent="">…</div>

{/* a page with native scroll throughout */}
import UkLenisOff from '@/components/uikit/UkLenisOff'
<UkLenisOff />

{/* an overlay of your own */}
import { lenis_destroy, lenis_init } from '@/es-scripts/LENIS/uk-lenis-init'
onOpen:  lenis_destroy()
onClose: lenis_init()

// or, with no import: window.dispatchEvent(new Event('lenis:destroy'))

The API (es-scripts/LENIS/uk-lenis-init.js)

  • lenis_init()function
    Creates the instance, unless one exists or [data-lenis-off] is in the DOM. Also window.lenis_init() and the event lenis:init.
  • lenis_destroy()function
    Removes the GSAP ticker, destroy(), window.lenis = null, strips the lenis* classes from <html>: native scroll. lenis:destroy.
  • lenis_pause()function
    lenis.stop(): wheel and touch blocked, instance kept. lenis:pause.
  • lenis_resume()function
    lenis.start(), or creates one if there is none. lenis:resume.
  • lenis_toggle()function
    Pause when running, resume when paused, create when missing. lenis:toggle.

Each is also on window and behind a window event of the same name (lenis:pause…). Pause, resume, toggle and destroy log to the console; window.__lenis_debug = true adds an on-screen badge.

UkLenisOff and UkLenisToggle

UkLenisOff takes no props and renders <span data-lenis-off="" hidden />. UkLenisToggle, the debug button:

  • labelstringdefault 'Lenis'
    The text before the state (Lenis: ON (smooth)).
  • stickybooleandefault false
    Fixed at the bottom corner (16px from the end and the bottom) instead of inline.

Known gap (17.10): the toggle reads window.lenis once, on mount, before the route init (50ms later) creates Lenis. On a page loaded directly it shows OFF while Lenis is on, and its first click only corrects the label. The window.lenis readout above is the true state.

Rules

  • A scroll box in the page gets data-lenis-prevent; a page with native scroll throughout gets <UkLenisOff />. Pause is not for a popup that scrolls: it cancels the popup's wheel too (why fancybox_init destroys Lenis instead).
  • Never window.lenis.destroy() by hand: lenis_destroy() also takes the GSAP ticker off and clears the classes on <html>.
  • Never give an element id="lenis": the browser exposes it as window.lenis, lenis_init() takes it for an instance and never starts.

Live in