Scroll, text and page-level animation: springy, attribute-driven, and always visible under reduced motion. Scroll this page to see most of them fire.
Scroll reveal
data-sf-reveal="fade|rise|scale|blur|slide-left|slide-right|flip|pop", plus data-sf-delay and data-sf-stagger on a parent. Soft.reveal.refresh() picks up new nodes.
fade
rise
scale
blur
slide-left
slide-right
flip
pop +200ms delay
Scroll progress and reading progress
.sf-fxm-progress[data-sf-progress]. Without a value it tracks the page (fixed to the top); with a selector it tracks that article. data-sf-scroller targets an inner scroller, as here.
Why calm interfaces feel faster
Speed is mostly a feeling. A page that answers instantly but jumps around reads as nervous; one that settles with a small overshoot reads as confident. The difference is rarely the milliseconds. It is the shape of the motion.
Springs let an element arrive slightly early, lean past its mark, and come home. Our eyes treat that as life. Linear easing treats it as a spreadsheet cell sliding into place.
Keep the amplitude small. A few percent of scale, a few pixels of travel. The goal is to make the interface feel like it has weight, not to put on a show.
Stagger when things belong together, so the eye can follow the order. Reveal when the reader arrives, not when the page loads, so the motion lands where the attention is.
Respect the people who ask for less. Under reduced motion every element simply appears, in place, ready to read.
Finally, measure by feel. If you notice the animation, it is probably too long. If you notice its absence, it was just right.
Scroll inside
Parallax layers
data-sf-parallax="0.3" (negative values move against the scroll; data-sf-parallax-axis="x"). Transform only, rAF throttled.
Layers drift at their own pace
Scroll inside
Pinned horizontal gallery and sticky sections
.sf-fxm-hscroll[data-sf-hscroll] pins while vertical scroll drives the track sideways (a native swipe strip under reduced motion). .sf-fxm-sticky[data-sf-pin] pins a visual while its steps scroll past. Both follow the page, or the nearest .sf-fxm-scroller window as here.
01Sketch the shape02Add a little spring03Stagger the family04Test with reduced motion05Ship it quietly
Scroll down to slide
Planoutline the story
Drawmake the shapes
Moveadd the springs
1. Plan
Decide what the reader should notice first, and let motion point there.
2. Draw
Keep shapes flat and round. Motion does the personality work.
3. Move
Small overshoot, short travel, one idea per movement.
Scroll inside
Scroll-linked animation
data-sf-scrollfx="scale|slide|fade|rotate|bar". Uses CSS animation-timeline: view() where supported, otherwise JS writes --sf-vp / --sf-vc for the same look.
Each card is tied to its position in this window.
scale up as I enter
slide in from the left
fade with scroll
rotate as I pass
Scroll inside
Text split and text effects
data-sf-text="reveal-up|mask|blur-in|wave" (optional data-sf-text-by="words|chars", data-sf-text-stagger). Soft.split(el, {by}) powers them and keeps a screen-reader copy.
Reveal up, word by word, with a little tilt
Mask reveal slides each word out of a slot
Blur in from the fog
Wave keeps rolling
Typewriter
data-sf-typewriter="One|Two|Three" with data-sf-loop, data-sf-speed, data-sf-pause, data-sf-caret="block|underscore".
We make
Scramble and decode
data-sf-scramble="hover|reveal|both", optional data-sf-to for a hover swap. Soft.scramble(el, newText) works from code.
decrypting the message on reveal
Both: on reveal and on hover
Flip words
data-sf-flipwords="fast|calm|springy", data-sf-interval. Add .sf-fxm-flip--pill for a filled pill. The box is sized to the longest word, so the line never jumps.
Interfaces that feel
Made for
Kinetic variable-weight text
data-sf-kinetic with data-sf-min, data-sf-max, data-sf-radius. Letters swell near the pointer. For true weight ripples load a variable font, e.g. <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@200..800&display=swap" rel="stylesheet">. Without it, Figtree is used and weights step between 400 to 700.
Feel the weight
Highlighter, underline and strike draw
data-sf-draw="highlight|underline|squiggle|strike" draws itself as it scrolls into view. Optional data-sf-delay.
Interfaces should feel alive but quiet, with one idea per motion, a playful wiggle where it counts, and nothing that shouts.
Marquee text
.sf-fxm-marquee[data-sf-marquee] with data-sf-speed, data-sf-direction, data-sf-reactive, data-sf-pause-hover. Scroll the page and it speeds up and flips direction with you.
CalmSpringyTinyRound
Trusted by small teamsOpen sourceNo dependenciesDark mode ready
Count-up and odometer
data-sf-countup="12800" (data-sf-decimals, -prefix, -suffix, -duration, -ease="spring|expo") and .sf-fxm-odo[data-sf-odometer="2048"] for rolling digits.
12,800+99.9%$2,0485.0/5
Gradient shimmer text and shine sweep
.sf-fxm-shimmer (--soft for a subtle grey) and .sf-fxm-shine on any button (--loop sweeps on its own).
Shimmering headlineThinking...
Text outline to fill
.sf-fxm-outline (--pink). Hover or focus to fill from the left; the letters relax as they fill.
OUTLINEFILL
Page transitions
data-sf-transition="fade|slide|zoom" on a link. Pointing at a .sf-fxm-view id swaps views with the View Transitions API (fallback: blur-fade). Links to other pages cover with a veil and fade back in on arrival. Soft.transition(fn, {target, type}) wraps any DOM change.
A crossfade with a tiny rise. The default for most navigation.
Pricing
Slides sideways, so it feels like moving to the next room.
Contact
Zooms through, good for entering a detail view.
Smooth anchors and active section
nav.sf-fxm-anchors[data-sf-anchors] with data-sf-offset and data-sf-scroller. Eased scroll, focus lands on the section, and a springy pill follows the section in view. Soft.scrollTo(target, {offset}) from code.
Intro
Click a link above. The scroll eases out, and the pill hops to the section you are reading.
Features
Everything is a data attribute away. No build step, no dependencies.
Pricing
Free forever. Seriously. Bring snacks.
FAQ
Yes it supports dark mode. Yes it respects reduced motion.
Scroll inside
Section entrance choreography
data-sf-sequence on a parent, data-sf-seq-step="rise|pop|..." on children, with data-sf-gap, data-sf-start, and a per-step data-sf-at (ms). Soft.sequence.replay(el) or a data-sf-replay button re-runs it.
New
Everything arrives in order
A badge, a headline, a line of copy, then the actions, each one a beat after the last.
Number ticker and live counter
data-sf-ticker="12|48|97" rolls through values; data-sf-live="1204" keeps climbing (data-sf-rate ms, data-sf-live-step="1-6"), firing sf:live. Both use the odometer and pause off screen.
12 days1204online now
Image and blob reveal with clip-path
.sf-fxm-clip[data-sf-clip="circle|inset|polygon|diamond|morph"] opens as it scrolls in; the content inside settles from a zoom. Works on any element, including real images.
circle
inset
polygon
diamond
morph
Scroll snap sections with dots
.sf-fxm-snap[data-sf-snap]: children snap to the top; JS adds a dot navigator with a stretching active dot. Dots are real buttons.
Helloscroll or click a dotSnapeach section locks into placeDotsthe active one stretchesDonethat is all, folks
Inertial smooth scroll and cursor follower
Opt-in, app-wide: <html data-sf-smooth> (or Soft.smoothScroll.enable(lerp)) and <body data-sf-cursor> (or Soft.cursor.enable()), or scoped to one box with .sf-fxm-cursor-zone[data-sf-cursor-zone] as here. Both are skipped for reduced motion; the cursor needs a fine pointer. Mark things with data-sf-cursor-label="View", data-sf-cursor-grow or data-sf-cursor-hide; use data-sf-smooth-ignore on a custom scroller.
Move your pointer in here
The follower lives only inside this box. Needs a mouse or trackpad.
Hover me for a label
Loading screen and preloader
.sf-fxm-preloader[data-sf-preloader] counts 0 to 100, then the curtain lifts with a curved edge. Full-screen by default (waits for page load); --contained keeps it in a box. data-sf-manual + data-sf-preloader-run="#id" to replay. Fires sf:preloader-done.
0
The page behind the curtain
Curtain and wipe between states
.sf-fxm-wipe with .sf-fxm-wipe__state children; a button with data-sf-wipe="#box" (and optional data-sf-to="2") sweeps a coloured curtain across, swaps state under it and pulls it away. data-sf-wipe-dir="up|down|left|right".
Draft
Your idea, still warm.
Review
Friends are poking at it.
Shipped
It is out in the world.
Monthly
Pay as you go.
Yearly
Two months free.
Scroll-triggered stats row
.sf-fxm-stats[data-sf-stagger] with .sf-fxm-stat cards; counters start when the row is in view, each a beat after the last.
48,000+Developers
4.9Average rating
12kbGzipped
$0Cost, forever
Sticky header, hide on scroll down
header[data-sf-hidebar] (.sf-fxm-header) slides away on the way down and springs back on the way up; data-sf-threshold, data-sf-offset, data-sf-scroller. It stays visible while it holds focus.
Brand
Scroll down: the header tucks away. Scroll up: it bounces back.
Block one
Block two
Block three
Block four
Block five
Scroll inside
Back to top with progress ring
button[data-sf-top] (.sf-fxm-top, add --inline to sit inside a box) shows after data-sf-threshold px, its ring fills with scroll progress, and click scrolls up with easing. Use without data-sf-scroller for the page.