Progreso neumorfismo
Barra hundida con relleno azul y anillo en relieve con cifra central, con role progressbar y simulación opcional.
Subiendo archivos34%
34%
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";
/** Progreso neumorfismo: barra hundida con relleno que brilla y anillo en relieve con cifra central. Usa role progressbar. Simulación opcional para la demostración. React + TypeScript, sin dependencias. */
export type ProgresoNeumorficoProps = {
value?: number;
label?: string;
/** Si no se pasa value, avanza solo para la demostración. */
simulate?: boolean;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcprn-root { box-sizing: border-box; display: grid; gap: 26px; width: 100%; max-width: 380px; padding: 28px; border-radius: 28px; background: #e0e5ec; color: #46506a; font: 700 14px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; box-shadow: 10px 10px 20px #a3b1c6, -10px -10px 20px #fff; }
.gcprn-root *, .gcprn-root *::before, .gcprn-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcprn-root * { transition-duration: .01ms !important; } }
.gcprn-head { display: flex; justify-content: space-between; margin-bottom: 12px; }
.gcprn-pct { font-variant-numeric: tabular-nums; color: #3b4658; }
.gcprn-track { height: 18px; padding: 3px; border-radius: 999px; box-shadow: inset 4px 4px 8px #a3b1c6, inset -4px -4px 8px #fff; }
.gcprn-fill { height: 100%; min-width: 12px; border-radius: 999px; background: linear-gradient(90deg, #6b97f5, #5b8def); box-shadow: 2px 2px 5px rgba(91, 141, 239, .5); transition: width .5s cubic-bezier(.16, 1, .3, 1); }
.gcprn-ringwrap { position: relative; justify-self: center; width: 160px; height: 160px; border-radius: 50%; box-shadow: 8px 8px 16px #a3b1c6, -8px -8px 16px #fff; display: grid; place-items: center; }
.gcprn-ringwrap::before { content: ""; position: absolute; inset: 14px; border-radius: 50%; box-shadow: inset 5px 5px 10px #a3b1c6, inset -5px -5px 10px #fff; }
.gcprn-ringwrap svg { position: absolute; inset: 14px; width: calc(100% - 28px); height: calc(100% - 28px); transform: rotate(-90deg); }
.gcprn-ringwrap circle { fill: none; stroke-width: 8; stroke-linecap: round; }
.gcprn-ringwrap .v { stroke: #5b8def; transition: stroke-dashoffset .5s cubic-bezier(.16, 1, .3, 1); filter: drop-shadow(0 0 3px rgba(91, 141, 239, .6)); }
.gcprn-num { position: relative; font: 800 32px/1 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.03em; color: #3b4658; font-variant-numeric: tabular-nums; }`;
export function ProgresoNeumorfico({ value, label = "Subiendo archivos", simulate = true, className = "", style }: ProgresoNeumorficoProps) {
const [auto, setAuto] = useState(34);
useEffect(() => {
if (value !== undefined || !simulate || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const id = window.setInterval(() => !document.hidden && setAuto((v) => (v >= 100 ? 4 : Math.min(100, v + 3 + Math.random() * 6))), 700);
return () => window.clearInterval(id);
}, [value, simulate]);
const v = Math.round(Math.max(0, Math.min(100, value ?? auto)));
const C = 2 * Math.PI * 56;
return (
<div className={`gcprn-root ${className}`} style={style}>
<style>{CSS}</style>
<div>
<div className="gcprn-head">
<span id="gcprn-l">{label}</span>
<span className="gcprn-pct">{v}%</span>
</div>
<div className="gcprn-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={v} aria-label={label}>
<div className="gcprn-fill" style={{ width: `${v}%` }} />
</div>
</div>
<div className="gcprn-ringwrap" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={v} aria-label={`${label}, anillo`}>
<svg viewBox="0 0 132 132" aria-hidden="true">
<circle className="v" cx="66" cy="66" r="56" strokeDasharray={C} strokeDashoffset={C * (1 - v / 100)} />
</svg>
<span className="gcprn-num">{v}%</span>
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/progreso-neumorficoEjemplo de uso de Progreso neumorfismo
Después de copiar progreso-neumorfico.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { ProgresoNeumorfico } from "./progreso-neumorfico";
export default function Ejemplo() {
return <ProgresoNeumorfico label="Ejemplo" simulate />;
}Propiedades de ProgresoNeumorfico
ProgresoNeumorfico acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
value? | number | Opcional. |
label? | string | Opcional. |
simulate? | boolean | Si no se pasa value, avanza solo para la demostración. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 60 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Progreso neumorfismo 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".