DAILY NEWS

Stay Ahead, Stay Informed – Every Day

Advertisement
Properties of scroll-timeline: creating animations on scroll without JavaScript



Stop Using JS for Scroll Animations: Meet Scroll-Timeline

Grab a coffee, friend. We need to talk about that heavy JavaScript library you are probably using just to make a header shrink or a progress bar move. It is 2026, and the days of hijacking the main thread with scroll listeners are officially over. We finally have scroll-timeline, and it is a total game-changer for both performance and developer sanity. Imagine creating complex parallax effects with the same ease as a simple hover transition.

How we suffered before

Remember the struggle? To create a simple parallax effect or a reading indicator, we had to attach an event listener to the window scroll. Then came the “scroll-jank” – that stuttering mess when the browser could not keep up with the JavaScript calculations and the rendering at the same time. We tried to fix it with requestAnimationFrame, debouncing functions, or bringing in heavy-duty libraries like ScrollMagic or GSAP. While those tools are powerful, they are often overkill for simple UI polish. We even spent time styling the scrollbar in all modern browsers just to make things look cohesive, but the logic remained bulky and JS-dependent. It was a lot of code for something that should have been native.

The modern way in 2026

Now, we have CSS Scroll-driven Animations. The core idea is simple: instead of an animation progressing over time (seconds), it progresses over scroll distance (pixels or percentage). Using scroll-timeline, we can define a named timeline on a scrollable container. Then, we link any element’s animation to that timeline using animation-timeline. It is declarative, it is readable, and most importantly, it runs off the main thread. If you have already mastered managing scroll behavior with overscroll-behavior, this is the natural next step in your CSS journey. You are no longer calculating offsets; you are just describing how things should look at the start and end of the scroll.

Ready-to-use code snippet

Here is a classic example: a reading progress bar that grows as you scroll down the page. Notice how we do not need a single line of script to make this happen.

/* 1. Define the animation as you normally would */
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}

/* 2. Setup the scroll container and name the timeline */
body {
scroll-timeline-name: –reading-timeline;
scroll-timeline-axis: block; /* ‘block’ refers to the vertical scroll axis */
}

/* 3. Link the progress bar element to the scroll timeline */
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 8px;
background: #ff4757;
transform-origin: 0 50%;
z-index: 100;

/* The magic happens here: no duration in seconds, but ‘auto’ */
animation: grow-progress auto linear;
animation-timeline: –reading-timeline;
}

Common beginner mistake

The most common pitfall is forgetting the animation-duration. Even though the animation is driven by scrolling and not time, the CSS specification still requires a duration value (set to auto or any time value like 1s) for the animation to actually initialize. If you omit it, your animation might just sit there doing nothing, leaving you scratching your head. Also, ensure your scroll-timeline-name is defined on an actual scrollable parent; if the container does not have overflow: auto or scroll (or it is the body), the timeline will not have any range to work with and your animation will stay stuck at the first frame.

🔥 We publish more advanced CSS tricks, ready-to-use snippets, and tutorials in our Telegram channel. Subscribe so you don’t miss out!



Source link

I built a startup waitlist landing page in Next.js 15 — here are all the decisions I made



I’ve been building Next.js templates as a side project and selling them on Gumroad. This weekend I shipped the fourth one: Orbit, a startup launch and waitlist landing page.

Here’s a breakdown of every technical decision I made.

Why Next.js 15 with CSS Modules (no Tailwind)

Most templates use Tailwind. That’s fine for customization, but it adds a compilation step and a learning curve for buyers who just want clean CSS they can read and edit.

CSS Modules give you:

Locally scoped class names (no conflicts)
Standard CSS syntax (no utility memorization)
Zero runtime cost
Works with Next.js out of the box

The tradeoff is more verbose than Tailwind for repetitive utilities. Worth it for a product you’re selling.

The bento grid — 1px gap trick

The features section uses CSS Grid with grid-template-columns: repeat(3, 1fr). The first card spans 2 columns via grid-column: span 2.

.bento {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(–color-border-subtle); /* gap IS the border */
border-radius: var(–radius-lg);
overflow: hidden;
}

.bento .card:first-child {
grid-column: span 2;
}

Enter fullscreen mode

Exit fullscreen mode

Instead of adding borders to each card, I set the grid’s background to the border color and use 1px gaps. The cards themselves have no borders. This gives perfectly consistent grid lines with zero extra markup.

Count-up animation with IntersectionObserver

The metrics section triggers a count-up when the section enters the viewport:

const observer = new IntersectionObserver(((entry)) => {
if (entry.isIntersecting && !started.current) {
started.current = true
const startTime = performance.now()

const tick = (now: number) => {
const progress = Math.min((now – startTime) / duration, 1)
const eased = 1 – Math.pow(1 – progress, 3) // cubic ease-out
setCount(Math.round(eased * end))
if (progress 1) requestAnimationFrame(tick)
}

requestAnimationFrame(tick)
}
}, { threshold: 0.4 })

Enter fullscreen mode

Exit fullscreen mode

The started ref prevents re-triggering if the user scrolls away and back. Cubic ease-out feels much more natural than linear. No library — 30 lines of TypeScript.

Infinite logo marquee (CSS-only)

.row {
display: flex;
gap: 64px;
width: max-content;
animation: marquee 24s linear infinite;
}

@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}

Enter fullscreen mode

Exit fullscreen mode

The key: duplicate the logos array in the component and animate exactly -50% (half the total width). Seamless loop. Edge fade via mask-image on the parent:

.track {
mask-image: linear-gradient(
to right,
transparent 0%, black 12%, black 88%, transparent 100%
);
}

Enter fullscreen mode

Exit fullscreen mode

Single content file

All editable content — name, copy, nav links, logos, features, metrics, testimonials, FAQ — lives in src/lib/constants.ts. The buyer touches one file and the whole page updates. No hunting through components.

Design tokens in globals.css

8 variables to rebrand the entire template:

:root {
–color-accent: #f59e0b; /* change this → full rebrand */
–color-bg: #09090b;
–font-display: ‘Sora’, sans-serif;
–font-body: ‘IBM Plex Sans’, sans-serif;
–radius-lg: 16px;
}

Enter fullscreen mode

Exit fullscreen mode

Connecting the waitlist form

The form ships with a simulated delay. Replace it with your stack:

/* ConvertKit */
await fetch(`https://api.convertkit.com/v3/forms/${FORM_ID}/subscribe`, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ api_key: KEY, email }),
})

/* Loops */
await fetch(‘https://app.loops.so/api/v1/contacts/create’, {
method: ‘POST’,
headers: { Authorization: `Bearer ${KEY}`, ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ email }),
})

Enter fullscreen mode

Exit fullscreen mode

Live demo: https://orbit-landing-iota.vercel.app/

The template is available on Gumroad for $29: https://devmaya.gumroad.com/l/orbit



Source link