Studio KitLoaders and Feedbackno dependencies

PulseLoader

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 props
64
1.8
3

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add pulse-loader

Writes 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

PropTypeDefault
size

Loader diameter in pixels.

number64
duration

Duration of one ring expansion in seconds.

number1.8
rings

Number of evenly staggered rings.

number3
color

Ring and core color.

string'#38bdf8'
label

Accessible label exposed via aria-label on the status element.

string'Loading'
className

Additional class merged onto the root element.

string''

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}