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.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/progreso-materialEjemplo 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.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
value? | number | Opcional. |
simulate? | boolean | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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".