An ambient loading glyph shaped as a row of leaf springs on a rail — one displacement keyframe, staggered per bar by negative animation-delay, reads as a single pulse of energy travelling down the rack rather than bars animating in isolation.
npx shadcn add https://design.helpmarq.com /r/loader-spring-bars.jsonregistry/core/loader-spring-bars/component.tsx"use client";
// ---------------------------------------------------------------------------
// LathRack — an ambient loading glyph shaped as a row of leaf springs
// mounted on a rail, one shared displacement passing through them left to
// right. Each lath runs the identical keyframe (compressed rest -> rise,
// past its own height, on ease-out-expo -> a two-beat decaying overshoot on
// the house spring curve -> back to rest) but at a negative animation-delay
// staggered by index, so what's actually one waveform looks like a pulse
// of energy travelling down the rack rather than N bars animating in
// isolation — the same "one rule, offset per element" trick loader-pendulum-sync
// uses for its pendulum row, here on translateY/scaleY instead of rotation.
// ---------------------------------------------------------------------------
const CSS = `
.ns-lr-rack{
display:flex;
align-items:flex-end;
justify-content:center;
gap:calc(var(--ns-lr-bar) * .6);
height:var(--ns-lr-height);
}
.ns-lr-bar{
width:var(--ns-lr-bar);
height:100%;
border-radius:9999px;
background:var(--foreground);
transform-origin:bottom center;
animation:ns-lr-pulse var(--ns-lr-period) infinite;
}
@keyframes ns-lr-pulse{
0%,18%{transform:scaleY(.38);animation-timing-function:cubic-bezier(.16,1,.3,1)}
30%{transform:scaleY(1.16);animation-timing-function:cubic-bezier(.34,1.56,.64,1)}
38%{transform:scaleY(.88);animation-timing-function:cubic-bezier(.34,1.56,.64,1)}
46%{transform:scaleY(1.05);animation-timing-function:cubic-bezier(.16,1,.3,1)}
54%,100%{transform:scaleY(.38)}
}
@media (prefers-reduced-motion: reduce){
.ns-lr-bar{animation:none;transform:scaleY(.7);opacity:.7;}
}
`;
export interface LathRackProps {
/** number of laths, clamped 3-9 */
count?: number;
/** rack height in px */
height?: number;
/** ms for one full pulse to sweep across the rack and repeat */
periodMs?: number;
/** text announced via the component's own aria-live region */
label?: string;
className?: string;
}
export function LathRack({
count = 5,
height = 40,
periodMs = 2200,
label = "Loading",
className = "",
}: LathRackProps) {
const n = Math.min(9, Math.max(3, Math.round(count)));
const bar = Math.max(3, Math.round(height * 0.11));
const step = periodMs * 0.085;
return (
<span
role="status"
aria-live="polite"
data-loader-spring-bars
className={`inline-flex items-end ${className}`}
style={{
height,
["--ns-lr-bar" as string]: `${bar}px`,
["--ns-lr-height" as string]: `${height}px`,
["--ns-lr-period" as string]: `${periodMs}ms`,
}}
>
<style>{CSS}</style>
<span className="ns-lr-rack" aria-hidden="true">
{Array.from({ length: n }, (_, i) => (
<span
key={i}
className="ns-lr-bar"
style={{ animationDelay: `${-(i * step)}ms` }}
/>
))}
</span>
<span className="sr-only">{label}</span>
</span>
);
}
A compact, always-on loading glyph (3-9 bars via a clamped `count` prop, sized via `height`) built as a row of rounded bars anchored at the bottom (`transform-origin:bottom center`) so they read as leaf springs mounted on a rail rather than a generic equalizer. Every bar runs the exact same `@keyframes` rule — rest compressed (scaleY .38) through a rise past its own resting height on ease-out-expo, a two-beat decaying overshoot on the house spring curve (cubic-bezier(.34,1.56,.64,1): stretched too far, corrects, small second bounce), back to rest — but each bar's `animation-delay` is a negative offset proportional to its index (~8.5% of `periodMs` per lath), so the identical waveform arrives at each bar slightly later than its left neighbor: the same 'one rule, phased per element' technique this registry's loader-pendulum-sync uses for its pendulum row, applied here to scaleY displacement instead of rotation. The visible result is one pulse of compression travelling left to right down the rack and looping, never N independent bars bouncing on their own clocks. Ink is `var(--foreground)` only, no gradients, no per-bar color. The whole glyph is a single `role="status" aria-live="polite"` element; the bar row is `aria-hidden` and a visually hidden text node (the `label` prop, default 'Loading') is the only thing announced — nothing here is interactive, so it renders zero controls and is correctly exempt from the tabbability check. `prefers-reduced-motion` removes the animation entirely, leaving every bar at a fixed mid-height with reduced opacity rather than either extreme of the pulse, so the resting frame reads as paused motion rather than an arbitrary freeze-frame. Props: count (3-9, default 5), height (px, default 40, bar width derives from it), periodMs (full pulse-sweep-and-repeat cycle, default 2200), label, className. Pure DOM + CSS, zero dependencies, no canvas, no framer-motion despite the source this was rebuilt from declaring it as a dependency it never actually imported.