sjgwebdev.Component Library

Components

Motion

Every duration, distance and easing comes from a token. Nothing hard-codes a number, which is why one change to theme.ts retimes a whole site.

Reveal

Scroll reveal

Scroll this panel ↓

Card 1

Slides in from the right, once. Jump down the panel and all three enter together, the stagger is what stops that reading as one block.

Card 2

Slides in from the right, once. Jump down the panel and all three enter together, the stagger is what stops that reading as one block.

Card 3

Slides in from the right, once. Jump down the panel and all three enter together, the stagger is what stops that reading as one block.

Load-in reveal

Load-in reveal

Rises 20px and fades as the page becomes visible.

Staggered reveal
Typing

npm run build

Split heading

Wordsriseintoplace

Heading emphasis

Chip wipe

Emphasis is a filled chip that settles.

Sticker press
Click and hold
Hover lift

Hover lift

Travels 2px. More reads as the card coming loose.

Accordion
First question

The panel snaps open. Only the marker rotates.

Second question

The panel snaps open. Only the marker rotates.

Numbers

Counter
0Page views
0+Projects
$0Saved
Counter, single
0%Lighthouse performance

Scroll-linked & continuous

Continuous motion tied to the scroll wheel is the strongest trigger for vestibular symptoms there is, so these are gated on the media query directly and stop entirely. No site-wide policy, no per-element opt-out.

Marquee
AstroTailwindCloudflareTypeScriptSupabaseGSAP
Reading progress

Scroll to fill the bar

The bar above tracks how far through this panel you are, and reaches 100% exactly as the last line reaches the bottom.

It is driven by one scroll listener, throttled with requestAnimationFrame so a trackpad flick writes once per frame rather than dozens of times per frame.

Every layout read happens before every write. Interleaving them forces the browser to recompute layout mid-frame, which is what turns a progress bar from free into a jank source.

The travel is the content height minus one viewport height. That subtraction is the part everyone leaves out.

Without it, the bar tops out around 80% on a long document and never fills, which reads as broken rather than as a design choice.

It is clamped at both ends, because rubber-band scrolling on iOS reports a negative position at the top and an over-height one at the bottom.

A ResizeObserver watches the panel too. Height changes without a scroll, images loading, a font swapping, a filter hiding rows, would otherwise leave the bar stale until the next move.

The bar is aria-hidden. It restates what the scrollbar already carries natively and more precisely, so it earns its place visually rather than semantically.

Announcing "thirty-seven percent" to a screen reader would be noise on top of a control that user already has.

On a real page this shares its listener with the scroll-to-top button and anything else that needs scroll position.

Two listeners each doing their own rAF throttle means two callbacks, two layout reads and two chances to force a synchronous reflow on every frame of every scroll.

On the one page type where scrolling smoothly is the entire product, that is not a trade worth making.

Keep scrolling, there is a little more, so the bar has somewhere left to go.

That is the end. The bar should be full.

Scroll-snap carousel
1Snap point
2Snap point
3Snap point
4Snap point
5Snap point
6Snap point
7Snap point
8Snap point

Elsewhere

Worth a look when CSS genuinely will not do it. The bar for adding a dependency is high. demo3 draws a rotating wireframe sphere in about 120 lines of vanilla canvas, against the 331 KB of libraries the design it follows uses for the same effect.