Concentric rings that expand and fade outward from a glowing core in an evenly staggered sequence, with configurable ring count, size and period.
Preview
Customize
5 props64
1.8
3
Installation
Copies the component source and its CSS Module into your project.
npx motionfoundry add pulse-loaderWrites components/ui/pulse-loader.tsx and components/styles/pulse-loader.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
example.tsx
1import { PulseLoader } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <PulseLoader />6 );7}Props
Accessibility
Root uses role="status" with an aria-label (default 'Loading'); rings and core are aria-hidden. Under prefers-reduced-motion the ripple stops and rings are shown as static concentric circles of decreasing opacity.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
components/ui/pulse-loader.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/pulse-loader.module.css";5 6export interface PulseLoaderProps {7 /** Loader diameter in pixels */8 size?: number;9 /** Duration of one ring expansion in seconds */10 duration?: number;11 /** Number of staggered rings */12 rings?: number;13 /** Ring and core color */14 color?: string;15 /** Accessible label announced by assistive technology */16 label?: string;17 /** Optional className merged onto the root */18 className?: string;19}20 21export function PulseLoader({22 size = 64,23 duration = 1.8,24 rings = 3,25 color = "#38bdf8",26 label = "Loading",27 className = "",28}: PulseLoaderProps) {29 const total = Math.max(1, Math.round(rings));30 const safeDuration = duration > 0 ? duration : 1.8;31 const styleVars = {32 "--mf-pl-size": `${size}px`,33 "--mf-pl-duration": `${safeDuration}s`,34 "--mf-pl-color": color,35 } as React.CSSProperties;36 37 return (38 <div role="status" aria-label={label} style={styleVars} className={`${styles.root} ${className}`.trim()}>39 {Array.from({ length: total }, (_, i) => (40 <span41 key={i}42 aria-hidden="true"43 className={styles.ring}44 style={45 {46 animationDelay: `-${((i * safeDuration) / total).toFixed(3)}s`,47 "--mf-pl-static": ((i + 1) / total).toFixed(3),48 } as React.CSSProperties49 }50 />51 ))}52 <span aria-hidden="true" className={styles.core} />53 </div>54 );55}