soft

Motion effects

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.

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

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 shape
02Add a little spring
03Stagger the family
04Test with reduced motion
05Ship it quietly

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-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

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,048 5.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 headline Thinking...

Text outline to fill

.sf-fxm-outline (--pink). Hover or focus to fill from the left; the letters relax as they fill.

OUTLINE FILL

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.

Overview

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.

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 days 1204online 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 dot
Snapeach section locks into place
Dotsthe active one stretches
Donethat 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.

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

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.

Scroll down inside this box to reveal the button.

One
Two
Three
Four
Five
Six