Ir al contenido
Evaluar mi proyecto
Componentes · Progreso y cargaDisponible

Progreso Material Design 3

Lineal determinado, lineal indeterminado y circular (determinado e indeterminado) con la paleta de Material 3.

Descargando40%
Buscando actualizaciones
40

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

progreso-material.tsx80 líneas
Descargar
"use client";

import { useEffect, useState, type CSSProperties } from "react";

/** Indicadores de progreso Material Design 3: lineal determinado, lineal indeterminado y circular, con colores de la paleta M3 y respeto a movimiento reducido. React + TypeScript, sin dependencias. */
export type ProgresoMaterialProps = {
  value?: number;
  simulate?: boolean;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcprm-root { box-sizing: border-box; display: grid; gap: 28px; width: 100%; max-width: 380px; color: #1d1b20; font: 400 14px/20px Roboto, "Google Sans", ui-sans-serif, system-ui, sans-serif; letter-spacing: .25px; }
.gcprm-root *, .gcprm-root *::before, .gcprm-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcprm-root * { animation: none !important; transition-duration: .01ms !important; } .gcprm-ind::before { width: 40% !important; left: 30% !important; } .gcprm-circ { animation: none !important; } }
.gcprm-lab { display: flex; justify-content: space-between; margin-bottom: 8px; color: #49454f; }
.gcprm-lin { position: relative; height: 4px; overflow: hidden; border-radius: 2px; background: #e8def8; }
.gcprm-lin > i { position: absolute; inset: 0 auto 0 0; border-radius: 2px; background: #6750a4; transition: width .4s cubic-bezier(.2, 0, 0, 1); }
.gcprm-lin > i::after { content: ""; position: absolute; right: -6px; top: 0; bottom: 0; width: 4px; border-radius: 2px; background: #6750a4; }
.gcprm-ind::before, .gcprm-ind::after { content: ""; position: absolute; top: 0; bottom: 0; border-radius: 2px; background: #6750a4; }
.gcprm-ind::before { animation: gcprm-a 2.1s cubic-bezier(.65, .815, .735, .395) infinite; }
.gcprm-ind::after { animation: gcprm-b 2.1s cubic-bezier(.165, .84, .44, 1) 1.15s infinite; }
.gcprm-circwrap { display: flex; align-items: center; gap: 28px; }
.gcprm-circ { width: 48px; height: 48px; animation: gcprm-rot 1.4s linear infinite; }
.gcprm-circ circle { fill: none; stroke: #6750a4; stroke-width: 4; stroke-linecap: round; animation: gcprm-dash 1.4s ease-in-out infinite; }
.gcprm-det { position: relative; width: 48px; height: 48px; }
.gcprm-det svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.gcprm-det circle { fill: none; stroke-width: 4; stroke-linecap: round; }
.gcprm-det .t { stroke: #e8def8; }
.gcprm-det .v { stroke: #6750a4; transition: stroke-dashoffset .4s cubic-bezier(.2, 0, 0, 1); }
.gcprm-det b { position: absolute; inset: 0; display: grid; place-items: center; font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; }
@keyframes gcprm-a { 0% { left: -35%; right: 100%; } 60% { left: 100%; right: -90%; } 100% { left: 100%; right: -90%; } }
@keyframes gcprm-b { 0% { left: -200%; right: 100%; } 60% { left: 107%; right: -8%; } 100% { left: 107%; right: -8%; } }
@keyframes gcprm-rot { to { transform: rotate(360deg); } }
@keyframes gcprm-dash { 0% { stroke-dasharray: 2 126; stroke-dashoffset: 0; } 50% { stroke-dasharray: 90 126; stroke-dashoffset: -30; } 100% { stroke-dasharray: 2 126; stroke-dashoffset: -126; } }`;

export function ProgresoMaterial({ value, simulate = true, className = "", style }: ProgresoMaterialProps) {
  const [auto, setAuto] = useState(40);
  useEffect(() => {
    if (value !== undefined || !simulate || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const id = window.setInterval(() => !document.hidden && setAuto((v) => (v >= 100 ? 8 : Math.min(100, v + 6 + Math.random() * 8))), 800);
    return () => window.clearInterval(id);
  }, [value, simulate]);
  const v = Math.round(Math.max(0, Math.min(100, value ?? auto)));
  const C = 2 * Math.PI * 20;
  return (
    <div className={`gcprm-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div>
        <div className="gcprm-lab">
          <span>Descargando</span>
          <span>{v}%</span>
        </div>
        <div className="gcprm-lin" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={v} aria-label="Descargando">
          <i style={{ width: `${v}%` }} />
        </div>
      </div>
      <div>
        <div className="gcprm-lab">
          <span>Buscando actualizaciones</span>
        </div>
        <div className="gcprm-lin gcprm-ind" role="progressbar" aria-label="Buscando actualizaciones" />
      </div>
      <div className="gcprm-circwrap">
        <svg className="gcprm-circ" viewBox="0 0 48 48" role="progressbar" aria-label="Cargando">
          <circle cx="24" cy="24" r="20" />
        </svg>
        <div className="gcprm-det" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={v} aria-label="Avance circular">
          <svg viewBox="0 0 48 48" aria-hidden="true">
            <circle className="t" cx="24" cy="24" r="20" />
            <circle className="v" cx="24" cy="24" r="20" strokeDasharray={C} strokeDashoffset={C * (1 - v / 100)} />
          </svg>
          <b>{v}</b>
        </div>
      </div>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/progreso-material

Ejemplo de uso de Progreso Material Design 3

Después de copiar progreso-material.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { ProgresoMaterial } from "./progreso-material";

export default function Ejemplo() {
  return <ProgresoMaterial simulate />;
}

Propiedades de ProgresoMaterial

ProgresoMaterial acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
value?numberOpcional.
simulate?booleanOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 80 líneas (5 KB) y define 4 animaciones con @keyframes. Esto es lo que hace Progreso Material Design 3 por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Declara roles ARIA para lectores de pantalla: progressbar.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)