Ir al contenido
Evaluar mi proyecto
Componentes · Progreso y cargaDisponible

Medidor de LED skeuomórfico

Segmentos que se encienden de verde a ámbar y rojo, con marca de pico y pantalla digital; para volumen, batería o carga.

Nivel

Código completo

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

progreso-led.tsx60 líneas
Descargar
"use client";

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

/** Medidor de LED skeuomórfico: segmentos que se encienden de verde a ámbar y rojo, con vidrio, tornillos y marca de pico. Sirve para volumen, batería o carga. React + TypeScript, sin dependencias. */
export type ProgresoLedProps = {
  value?: number;
  segments?: number;
  label?: string;
  simulate?: boolean;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcprl-root { box-sizing: border-box; display: inline-grid; gap: 14px; max-width: 100%; padding: 18px 22px 16px; border-radius: 14px; background: linear-gradient(180deg, #34363c, #202226); color: #c9ccd4; font: 700 11px/1 "Helvetica Neue", Arial, ui-sans-serif, sans-serif; letter-spacing: .16em; text-transform: uppercase; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14), inset 0 -2px 6px rgba(0, 0, 0, .5), 0 14px 22px -10px rgba(0, 0, 0, .7); }
.gcprl-root *, .gcprl-root *::before, .gcprl-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcprl-root * { transition-duration: .01ms !important; } }
.gcprl-head { display: flex; justify-content: space-between; align-items: center; gap: 28px; }
.gcprl-val { padding: 4px 8px; border-radius: 4px; background: #0b140d; color: #7dffa1; font: 600 14px/1 ui-monospace, "SF Mono", Menlo, monospace; text-shadow: 0 0 6px rgba(125, 255, 161, .6); box-shadow: inset 0 1px 3px rgba(0, 0, 0, .9); font-variant-numeric: tabular-nums; }
.gcprl-bar { display: flex; gap: 4px; padding: 8px; border-radius: 8px; background: #0d0e11; box-shadow: inset 0 2px 6px rgba(0, 0, 0, .9), 0 1px 0 rgba(255, 255, 255, .12); }
.gcprl-seg { flex: 1; min-width: 10px; height: 34px; border-radius: 3px; background: color-mix(in srgb, var(--c) 18%, #14151a); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05); transition: background .12s, box-shadow .12s; }
.gcprl-seg[data-on="true"] { background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--c) 60%, #fff), var(--c) 70%); box-shadow: 0 0 10px 1px color-mix(in srgb, var(--c) 70%, transparent), inset 0 1px 0 rgba(255, 255, 255, .5); }
.gcprl-seg[data-peak="true"] { background: color-mix(in srgb, var(--c) 55%, #14151a); }`;

export function ProgresoLed({ value, segments = 16, label = "Nivel", simulate = true, className = "", style }: ProgresoLedProps) {
  const [auto, setAuto] = useState(55);
  const [peak, setPeak] = useState(0);
  useEffect(() => {
    if (value !== undefined || !simulate || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const id = window.setInterval(() => !document.hidden && setAuto((v) => Math.max(8, Math.min(100, v + (Math.random() - 0.5) * 46))), 260);
    return () => window.clearInterval(id);
  }, [value, simulate]);
  const v = Math.round(Math.max(0, Math.min(100, value ?? auto)));
  const lit = Math.round((v / 100) * segments);
  const litRef = useRef(lit);
  useEffect(() => {
    litRef.current = lit;
  }, [lit]);
  useEffect(() => {
    const id = window.setInterval(() => setPeak((p) => (litRef.current >= p ? litRef.current : Math.max(litRef.current, p - 1))), 260);
    return () => window.clearInterval(id);
  }, []);
  const color = (i: number) => (i / segments > 0.85 ? "#ff4d3a" : i / segments > 0.65 ? "#ffb82e" : "#38e07b");
  return (
    <div className={`gcprl-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcprl-head">
        <span>{label}</span>
        <span className="gcprl-val" aria-hidden="true">{String(v).padStart(3, "0")}</span>
      </div>
      <div className="gcprl-bar" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={v} aria-label={label}>
        {Array.from({ length: segments }, (_, i) => (
          <span key={i} className="gcprl-seg" style={{ "--c": color(i) } as CSSProperties} data-on={i < lit} data-peak={i === peak - 1 && peak > lit} />
        ))}
      </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-led

Ejemplo de uso de Medidor de LED skeuomórfico

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

ejemplo.tsx5 líneas
import { ProgresoLed } from "./progreso-led";

export default function Ejemplo() {
  return <ProgresoLed label="Ejemplo" simulate />;
}

Propiedades de ProgresoLed

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

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

Accesibilidad y rendimiento

El archivo tiene 60 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Medidor de LED skeuomórfico 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.
  • Calcula tonos con color-mix(), así que un solo color base alcanza.
  • 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)