Ir al contenido
Evaluar mi proyecto
Componentes · Mundos de estiloDisponible

Mundo glassmorfismo: panel de control

Clima, reproductor y chips de conexión en vidrio esmerilado sobre un cielo de color con manchas desenfocadas.

Guadalajara

Parcialmente soleado

24°

Ahora24°
15h26°
17h22°
19h20°
Horizonte nocturnoMarea Alta

Código completo

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

mundo-glass.tsx111 líneas
Descargar
"use client";

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

/** Mundo glassmorfismo: panel de control sobre un cielo de color con tarjeta de clima, reproductor y chips de estado en vidrio esmerilado. El reproductor y los chips responden. React + TypeScript, sin dependencias. */
export type MundoGlassProps = {
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcmgl-root { box-sizing: border-box; container-type: inline-size; position: relative; overflow: hidden; width: 100%; padding: clamp(18px, 4cqi, 40px); background: linear-gradient(135deg, #5b3fff, #c13fd6 50%, #ff7a8a); color: #fff; font: 600 14px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; text-shadow: 0 1px 2px rgba(30, 10, 80, .25); }
.gcmgl-root *, .gcmgl-root *::before, .gcmgl-root *::after { box-sizing: border-box; }
.gcmgl-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcmgl-root :focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .gcmgl-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcmgl-orb { position: absolute; border-radius: 50%; filter: blur(30px); opacity: .75; }
.gcmgl-o1 { width: 260px; height: 260px; left: -60px; top: -40px; background: #ffd166; }
.gcmgl-o2 { width: 300px; height: 300px; right: -80px; bottom: -80px; background: #3dd5ff; }
.gcmgl-grid { position: relative; display: grid; grid-template-columns: 1.1fr 1fr; gap: 16px; max-width: 760px; margin: 0 auto; }
.gcmgl-card { padding: 20px; border-radius: 26px; background: rgba(255, 255, 255, .16); -webkit-backdrop-filter: blur(20px) saturate(1.6); backdrop-filter: blur(20px) saturate(1.6); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .32), inset 0 1px 0 rgba(255, 255, 255, .55), 0 24px 40px -22px rgba(20, 10, 80, .6); }
.gcmgl-w { grid-row: span 2; display: grid; align-content: space-between; gap: 18px; min-height: 280px; }
.gcmgl-city { margin: 0; font-size: 18px; font-weight: 700; }
.gcmgl-t { margin: 0; font: 200 86px/.9 ui-sans-serif, system-ui, -apple-system, sans-serif; letter-spacing: -.05em; }
.gcmgl-hrs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; text-align: center; font-size: 13px; }
.gcmgl-hrs div { padding: 10px 0; border-radius: 14px; background: rgba(255, 255, 255, .14); }
.gcmgl-hrs b { display: block; margin-top: 4px; font-weight: 600; }
.gcmgl-pl { display: grid; gap: 14px; }
.gcmgl-song { display: flex; align-items: center; gap: 12px; }
.gcmgl-cover { flex: none; width: 52px; height: 52px; border-radius: 14px; background: conic-gradient(from 20deg, #ffd166, #ff7a8a, #8a6dff, #3dd5ff, #ffd166); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5); }
.gcmgl-song span { display: block; font-size: 12px; opacity: .85; font-weight: 500; }
.gcmgl-bar { height: 5px; border-radius: 3px; background: rgba(255, 255, 255, .28); overflow: hidden; }
.gcmgl-bar i { display: block; height: 100%; background: #fff; border-radius: 3px; transition: width .3s linear; }
.gcmgl-ctl { display: flex; justify-content: center; gap: 12px; }
.gcmgl-b { display: grid; place-items: center; width: 46px; height: 46px; border: 0; border-radius: 50%; background: rgba(255, 255, 255, .2); color: #fff; cursor: pointer; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .35); transition: transform .3s cubic-bezier(.34, 1.4, .64, 1), background .2s; }
.gcmgl-b:hover { transform: scale(1.08); background: rgba(255, 255, 255, .3); }
.gcmgl-b[data-main="true"] { width: 54px; height: 54px; background: #fff; color: #5b3fff; text-shadow: none; }
.gcmgl-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.gcmgl-chip { display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 14px; border: 0; border-radius: 999px; background: rgba(255, 255, 255, .14); color: #fff; cursor: pointer; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .3); transition: background .2s; }
.gcmgl-chip[aria-pressed="true"] { background: rgba(255, 255, 255, .92); color: #3f2bd1; text-shadow: none; }
@container (max-width: 620px) { .gcmgl-grid { grid-template-columns: minmax(0, 1fr); } .gcmgl-w { grid-row: auto; min-height: 0; } }`;

export function MundoGlass({ className = "", style }: MundoGlassProps) {
  const [playing, setPlaying] = useState(true);
  const [t, setT] = useState(34);
  const [chips, setChips] = useState({ wifi: true, bt: false, nube: true });
  useEffect(() => {
    if (!playing || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const id = window.setInterval(() => !document.hidden && setT((v) => (v >= 100 ? 0 : v + 1)), 600);
    return () => window.clearInterval(id);
  }, [playing]);
  const chip = (k: keyof typeof chips, label: string) => (
    <button className="gcmgl-chip" type="button" aria-pressed={chips[k]} onClick={() => setChips((p) => ({ ...p, [k]: !p[k] }))}>
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4" /></svg>
      {label}
    </button>
  );
  return (
    <div className={`gcmgl-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcmgl-orb gcmgl-o1" aria-hidden="true" />
      <div className="gcmgl-orb gcmgl-o2" aria-hidden="true" />
      <div className="gcmgl-grid">
        <section className="gcmgl-card gcmgl-w" aria-label="Clima">
          <div>
            <h3 className="gcmgl-city">Guadalajara</h3>
            <p style={{ margin: 0, opacity: .9 }}>Parcialmente soleado</p>
          </div>
          <p className="gcmgl-t">24°</p>
          <div className="gcmgl-hrs">
            {[["Ahora", "24°"], ["15h", "26°"], ["17h", "22°"], ["19h", "20°"]].map(([h, v]) => (
              <div key={h}>
                {h}
                <b>{v}</b>
              </div>
            ))}
          </div>
        </section>
        <section className="gcmgl-card gcmgl-pl" aria-label="Reproductor">
          <div className="gcmgl-song">
            <span className="gcmgl-cover" aria-hidden="true" />
            <div>
              Horizonte nocturno
              <span>Marea Alta</span>
            </div>
          </div>
          <div className="gcmgl-bar" role="progressbar" aria-label="Posición" aria-valuemin={0} aria-valuemax={100} aria-valuenow={t}><i style={{ width: `${t}%` }} /></div>
          <div className="gcmgl-ctl">
            <button className="gcmgl-b" type="button" aria-label="Anterior" onClick={() => setT(0)}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5h2v14H6zM20 5v14L9 12z" /></svg>
            </button>
            <button className="gcmgl-b" data-main="true" type="button" aria-label={playing ? "Pausar" : "Reproducir"} aria-pressed={playing} onClick={() => setPlaying((v) => !v)}>
              <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">{playing ? <path d="M7 5h3.500v14H7zM13.500 5H17v14h-3.500z" /> : <path d="M8 5v14l11-7z" />}</svg>
            </button>
            <button className="gcmgl-b" type="button" aria-label="Siguiente" onClick={() => setT(0)}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 5h2v14h-2zM4 5l11 7L4 19z" /></svg>
            </button>
          </div>
        </section>
        <section className="gcmgl-card" aria-label="Conexiones">
          <div className="gcmgl-chips">
            {chip("wifi", "Wi-Fi")}
            {chip("bt", "Bluetooth")}
            {chip("nube", "Nube")}
          </div>
        </section>
      </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/mundo-glass

Ejemplo de uso de Mundo glassmorfismo: panel de control

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

ejemplo.tsx5 líneas
import { MundoGlass } from "./mundo-glass";

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

Propiedades de MundoGlass

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

PropiedadTipoDescripción
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 111 líneas (8 KB) y no necesita @keyframes. Esto es lo que hace Mundo glassmorfismo: panel de control por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: progressbar.
  • Usa backdrop-filter para el efecto de vidrio; en navegadores sin soporte el panel se ve sin desenfoque.
  • 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)