Layouts & Motion · v4

Grid system and
scroll behaviour.

Two systems documented here. The grid governs spatial organisation — column counts, gutters, base unit — across digital and print contexts. The motion layer sits above it: Locomotive Scroll v5 with every attribute and configuration option as a live interactive reference. Scroll through the page to trigger each demo.

Grid system

Spatial organisation.

Two grids cover the full range of Latency materials. Digital uses a 12-column structure on an 8px base unit. Print uses a 6-column structure on a 7mm base unit. Full column-width calculations, margin specifications, and responsive behaviour are in development.

Specification in progress

Grid · Digital · 12-column Digital grid

12 columns on an 8px base unit. Gutters scale from 16px (mobile) to 40px (desktop). Maximum content width 1280px. All component widths and spacing in the Albufera system are derived from this grid.

12-column · 40px gutters · 1280px max-width

8pxBase unit
12Columns
16→40pxGutters
1280pxMax width
5Breakpoints
Grid · Print · A4 · 6-column Print grid

6 columns on a 7mm base unit. Outer margins 20mm, inner margins 15mm on A4. A 4-column variant covers narrow-format print (DL, A5). All print materials — decks, reports, one-pagers — derive from this structure.

6-column · 7mm base · A4

7mmBase unit
6Columns
20mmOuter margin
A4 / LetterFormat
Grid · Breakpoints · Responsive Breakpoints

Five breakpoints. Column count drops at smaller viewports; gutters narrow proportionally. The 1280px breakpoint is the primary design target for desktop marketing materials.

SizeWidthColsGutter
xs360px416px
sm768px824px
md1024px1232px
lg1280px1240px
xl1440px1240px

Locomotive Scroll

Scroll-driven motion.

Locomotive Scroll v5 — built on Lenis. 9.4kB gzipped. Smooth easing, viewport detection, per-element parallax, progress tracking as CSS variables or JS events, custom scroll calls, and anchor navigation. Every attribute is demonstrated live on this page — scroll through each section to see it working.

v5 · Lenis 1.3.17 9.4kB gzip TypeScript IntersectionObserver RAF
Setup · CDN · npm Installation & initialisation

One script tag, one constructor call. All options have working defaults — the minimal init is sufficient for most use cases. Pass a configuration object to override. This page uses the CDN approach.

CDN

<script src="https://cdn.jsdelivr.net/npm/locomotive-scroll/bundled/locomotive-scroll.min.js"></script>

Full configuration — all options shown

const scroller = new LocomotiveScroll({ lenisOptions: { lerp: 0.1, // smoothing intensity 0–1 duration: 1.2, // animation duration in seconds orientation: 'vertical', // 'vertical' | 'horizontal' gestureOrientation:'vertical', // 'vertical' | 'horizontal' | 'both' smoothWheel: true, smoothTouch: false, // keep false for native mobile feel wheelMultiplier: 1, touchMultiplier: 2, normalizeWheel: true, // cross-browser delta normalisation easing: (t) => 1 - Math.pow(1 - t, 3), // cubic-out }, triggerRootMargin: '-1px -1px -1px -1px', // IntersectionObserver margin rafRootMargin: '100% 100% 100% 100%', // RAF activation zone autoStart: true, scrollCallback: ({ scroll, limit, velocity, direction, progress }) => { // fires every RAF frame while scrolling }, });
Config · Real-time · Callback scrollCallback

Called on every RAF frame while the page is scrolling. Returns scroll (px), limit (max scroll px), velocity (signed, peaks ~3 on fast flicks), direction (1 down / −1 up), and progress (0–1 page fraction). The readout below is driven by this page's own scrollCallback.

Live scroll state — scroll to update

scroll0 px
limit
velocity0.000
direction
progress0.000

Velocity magnitude bar (0–3 normalised)

Config · Lenis · Feel lenisOptions

lerp is the primary feel control — lower values feel more fluid, higher values snappier. smoothTouch defaults off to preserve native momentum on iOS and Android. Do not change without testing on real devices.

lenisOptions: { lerp: 0.1, // ← fluid ···· snappy → 1.0 duration: 1.2, // affects inertia tail smoothWheel: true, smoothTouch: false, // keep false on mobile wheelMultiplier: 1, // scroll speed multiplier touchMultiplier: 2, normalizeWheel: true, // unifies Chrome/Firefox delta }

This page runs at lerp: 0.08 — slightly softer than the library default. Adjust per context; landing pages typically want 0.06–0.10, documentation 0.10–0.15.

In-view detection

Viewport triggers.

Five attributes govern element detection. data-scroll registers the element. data-scroll-class names the added class (default is-inview). data-scroll-repeat enables toggle on exit. data-scroll-position sets the trigger point. data-scroll-offset shifts it. data-scroll-ignore-fold disables automatic in-fold adjustment.

Detection · Entry · Class data-scroll

Enables Locomotive Scroll viewport detection. When the element enters the viewport the class is-inview is added and never removed — a one-shot trigger. No value required; presence of the attribute is sufficient. The four stat cards below use this to animate in.

Scroll through — cards reveal individually

Coverage

52M+

Financials

12M

Taxonomy

1,200

Integrations

80+

<div data-scroll> <!-- receives class "is-inview" on entry --> </div> /* CSS: base hidden state → revealed state */ [data-scroll] { opacity:0; transform:translateY(24px); transition: ... } [data-scroll].is-inview { opacity:1; transform:none; }
Detection · Custom class data-scroll-class

Replaces the default is-inview with a custom class name. Use when the trigger class needs to match an existing CSS state machine or when different entry animations are required on the same page.

Gains is-branded on entry

Class state

Scroll here to trigger is-branded — the border and label colour change via CSS alone.

<div data-scroll data-scroll-class="is-branded" ></div>
Detection · Repeat · Toggle data-scroll-repeat

Without this attribute, is-inview is added once and stays. With it, the class toggles: added on entry, removed on exit. The badge below scales up each time you scroll it into view.

Scroll away and back — re-triggers each time

In view — count: 0
data-scroll-repeat ← no value needed
Detection · Timing · Position data-scroll-position

Two comma-separated values specifying which part of the element (start / middle / end) must cross which part of the viewport (start / middle / end) to trigger. Default: start,end — element top crosses viewport bottom.

start,end

Top edge crosses viewport bottom — triggers first

middle,end

Centre crosses viewport bottom — triggers when half visible

end,end

Bottom edge crosses viewport bottom — triggers last

data-scroll-position="start,end" ← default data-scroll-position="middle,end" data-scroll-position="end,end"
Detection · Offset · px / % data-scroll-offset

Shifts the trigger threshold. Two comma-separated values: entry offset and exit offset. Accepts pixels or viewport-relative percentages. Positive delays trigger; negative brings it earlier.

0,0

No offset — triggers at natural edge

150,0

+150px — must travel 150px further into viewport

-80,0

−80px — triggers 80px before the natural edge

data-scroll-offset="0,0" ← default data-scroll-offset="150,0" ← delay px data-scroll-offset="-80,0" ← early px data-scroll-offset="25%,0" ← % of viewport
Detection · Fold · Offset adjustment data-scroll-ignore-fold

Locomotive Scroll automatically adjusts offsets for elements visible on initial page load (in-fold) to prevent them triggering immediately. data-scroll-ignore-fold disables this — the element uses raw offset values regardless of its initial position. Use when a hero element should animate in even though it starts in the viewport.

<!-- Default: offset auto-corrected for in-fold elements --> <div data-scroll data-scroll-offset="100,0"></div> <!-- Ignore fold: offset applied as specified, no adjustment --> <div data-scroll data-scroll-offset="100,0" data-scroll-ignore-fold ></div>

Parallax

Scroll speed displacement.

data-scroll-speed displaces an element relative to the scroll container. Positive values move counter to scroll (element rises as page scrolls down). Negative values amplify downward movement. Formula: displacement = progress × containerSize × speed × −1. Disabled on touch devices by default.

Motion · Parallax · Speed data-scroll-speed

Decimal. Start at 0.1–0.5 for subtle depth. The three labelled layers in the demo below run at 0.5, 0, and −0.4 — scroll through slowly to feel the separation.

speed: 0.5 — slow layer

Counter-scroll lift

speed: 0 — no parallax

Normal scroll

speed: −0.4 — fast layer

Accelerated sink

Parallax · Reference · Values Speed value reference

Common speed values and their practical application. Keep foreground copy at or near 0. Use larger values only for decorative background elements.

SpeedEffect
1.0Strong counter-lift — background imagery only
0.5Medium lift — hero backgrounds, decorative shapes
0.1Subtle drift — supporting elements near copy
0No parallax — body copy, interactive elements
−0.2Mild sink — adds depth on card stacks
−0.6Fast sink — use sparingly, decorative only
Parallax · Touch · Mobile data-scroll-enable-touch-speed

Parallax is suppressed on touch devices by default, preserving native scroll. Adding this attribute re-enables it per element. Test on target devices before using — lower-end hardware may not sustain smooth RAF under continuous parallax load.

<!-- Desktop-only parallax (default) --> <div data-scroll data-scroll-speed="0.5" ></div> <!-- Parallax on touch too --> <div data-scroll data-scroll-speed="0.5" data-scroll-enable-touch-speed ></div>

Touch parallax can cause jank on lower-end devices. The default off state is the safer production choice. Enable only after performance testing.

Progress tracking

Element-level scroll progress.

Two attributes expose per-element scroll progress as a 0–1 value. data-scroll-css-progress sets a --progress inline CSS variable — updated every RAF frame, drives animations with zero JS. data-scroll-event-progress fires a scroll-event-progress CustomEvent for JS-driven effects.

Progress · CSS variable · RAF data-scroll-css-progress

Sets --progress as an inline CSS custom property on the element, updated every animation frame as it moves through the rafRootMargin. CSS custom properties inherit — child elements can use var(--progress) directly. The fills below are driven purely by CSS.

Scroll through — fills track element progress

0.000 0.000 1.000
<div data-scroll data-scroll-css-progress> <!-- --progress available to all descendants --> <div class="fill"></div> </div> /* Pure-CSS animation */ .fill { width: calc(var(--progress, 0) * 100%); }
Progress · JS event · RAF data-scroll-event-progress

Fires a scroll-event-progress CustomEvent on window every frame, with detail: { target, progress }. The ring gauge below listens for this event. Use for GSAP scrubs, canvas renders, or any JS-driven scroll animation.

Scroll through — ring tracks element progress

0%
<div data-scroll data-scroll-event-progress="myEl" ></div> window.addEventListener('scroll-event-progress', (e) => { const { target, progress } = e.detail; // progress: 0 → 1 });

Custom events

Scroll-triggered callbacks.

data-scroll-call fires a scroll-call CustomEvent when an element enters or exits the viewport. The event detail returns the call ID, the direction (enter / exit), and the axis the element was crossed from. Use for analytics, video autoplay, lazy loading, or staged animation sequences.

Events · Call · Viewport data-scroll-call

Three trigger elements below each fire a different call ID. Scroll through them — each entry and exit is logged. data-scroll-repeat is required to receive both enter and exit events.

Call: sectionAlpha
Call: sectionBeta
Call: sectionGamma
Waiting for scroll events…
Events · Handler · Code scroll-call handler

Listen on window. The detail object always contains the call ID, the entry/exit state, and the crossing direction. Filter by ID when multiple elements share the same page.

// HTML <div data-scroll data-scroll-call="hero" data-scroll-repeat ← required for exit events ></div> // JS window.addEventListener('scroll-call', (e) => { const { id, way, from } = e.detail; // id → value of data-scroll-call // way → 'enter' | 'exit' // from → 'top' | 'bottom' if (id === 'hero' && way === 'enter') { startHeroAnimation(); } });

Anchor navigation

Smooth programmatic scroll.

Four attributes handle click-triggered smooth scrolling. data-scroll-to intercepts the click and delegates to Lenis. For <a> elements the href is used as target; for any other element use data-scroll-to-href. data-scroll-to-offset adds vertical clearance from the target. data-scroll-to-duration overrides animation duration.

Navigation · Anchor · Lenis data-scroll-to

The buttons below scroll to each section of this page using data-scroll-to. All use the standard <a href="#id"> pattern — no extra configuration needed for basic use.

<!-- Anchor — href is the scroll target --> <a href="#section-id" data-scroll-to>Scroll</a> <!-- Button with explicit target + options --> <button data-scroll-to data-scroll-to-href="#section-id" data-scroll-to-offset="80" ← px clearance from top data-scroll-to-duration="1.4" ← seconds >Scroll</button> // Programmatic scroll (JS) scroller.scrollTo('#section-id', { offset: -80, duration: 1.4 });
Notes · Limitations · Lenis Known constraints

SSR incompatibility

Locomotive Scroll depends on window and IntersectionObserver — both unavailable in server-side rendering contexts. Initialise only on the client, wrapped in a browser check.

Third-party popups

Dynamically injected modals can conflict with Lenis scroll capture. Use data-lenis-prevent on static scrollable containers. For dynamic content, use the prevent option in Lenis config.

Dynamic content

If the page height changes after init (content added, fonts loaded, images rendered), call scroller.resize() or re-initialise to recalculate scroll limits and element positions.

Latency · latencydata.com

Decoding private markets.