Cristal

El material, no la barra: velo, desenfoque, filo iluminado y un brillo que sigue al puntero.

Leer la versión en texto src/components/cristal.css

Qué es

Nació como la navbar de wearefaber.com y subió al sistema con nombre genérico: lo que hace que aquello se lea como cristal no tiene nada que ver con navegar. Sirve igual para una barra, una ficha flotante o un dock.

Son cuatro capas y las cuatro hacen falta: el velo translúcido de tinta con su degradado vertical, el desenfoque saturado de lo que pasa por debajo, el filo —luz en el canto de arriba, sombra en el de abajo, porque un cristal tiene grosor y sus dos caras no reciben la misma luz— y el brillo especular que sigue al puntero.

Un cristal se reconoce por cómo refleja lo que tiene detrás. Sobre un fondo plano y quieto no es cristal: es una caja gris. Esa frase es toda la regla de uso de esta pieza.

Cómo se importa

@import "@pantherkit/design-system/tokens.css";  /* siempre antes */
@import "@pantherkit/design-system/components.css";

El marcado, vivo

Lo que se ve aquí abajo y lo que hay en el bloque de código son el mismo texto: se inyecta y se imprime desde una sola fuente. Si se separaran, el que se copia sería el que nadie mira.

Una barra, con contenido pasando por detrás

Es el uso para el que nació. La barra está pegada arriba y el contenido le desfila por debajo: el desenfoque tiene algo que desenfocar y el material se lee como lo que es. Baja la rueda dentro del recuadro.

Baja la rueda dentro de este recuadro

Un cristal se reconoce por cómo refleja lo que tiene detrás. Este texto existe para tener algo que pase por debajo de la pieza: si no se mueve nada, el desenfoque no desenfoca nada.

Los bloques de arriba están para que pase algo con contraste por debajo del velo. En una página real ese contraste lo pone el contenido: un hero, una imagen, una rejilla de tarjetas.

Al final de la página no hay nada de esto. Por eso el footer de cristal se probó y se retiró.

El recuadro con scroll es andamiaje de la vitrina, no parte de la pieza. En producción el contenido de detrás es la página entera y la barra va fixed.

El marcado
<nav class="cristal flex items-center gap-4 px-5 py-3" data-cristal-luz>
  <span class="cristal__brillo" aria-hidden="true"></span>
  <span class="font-mono text-xs uppercase tracking-[0.18em]">Faber</span>
  <span class="flex-1"></span>
  <a href="/componentes" class="text-sm text-text-secondary">Componentes</a>
  <button type="button" class="fbtn fbtn--primario fbtn--compacto">Hablemos</button>
</nav>

Denso

Cuando la barra se despega del contenido —al hacer scroll— gana tinta y proyecta sombra. Es el mismo material con más densidad, no otro color: pasar a fondo claro sobre un hero oscuro se percibiría como un parpadeo.

Baja la rueda dentro de este recuadro

Un cristal se reconoce por cómo refleja lo que tiene detrás. Este texto existe para tener algo que pase por debajo de la pieza: si no se mueve nada, el desenfoque no desenfoca nada.

Los bloques de arriba están para que pase algo con contraste por debajo del velo. En una página real ese contraste lo pone el contenido: un hero, una imagen, una rejilla de tarjetas.

Al final de la página no hay nada de esto. Por eso el footer de cristal se probó y se retiró.

Mismo andamiaje. En la landing este estado lo dispara .is-scrolled desde el layout.

El marcado
<nav class="cristal cristal--denso flex items-center gap-4 px-5 py-3" data-cristal-luz>
  <span class="cristal__brillo" aria-hidden="true"></span>
  <span class="font-mono text-xs uppercase tracking-[0.18em]">Faber</span>
  <span class="flex-1"></span>
  <a href="/tokens" class="text-sm text-text-secondary">Tokens</a>
</nav>

Una ficha flotante, con el radio cambiado

El material no sabe qué forma tiene. Se le cambia el radio desde fuera con la perilla y sirve para un panel sin tocar ni una capa. La píldora es el defecto porque el primer uso fue una barra, no porque el cristal sea redondo.

Run activo

El material no sabe si es una barra o un panel. Solo sabe que tiene algo detrás.

Baja la rueda dentro de este recuadro

Un cristal se reconoce por cómo refleja lo que tiene detrás. Este texto existe para tener algo que pase por debajo de la pieza: si no se mueve nada, el desenfoque no desenfoca nada.

Los bloques de arriba están para que pase algo con contraste por debajo del velo. En una página real ese contraste lo pone el contenido: un hero, una imagen, una rejilla de tarjetas.

Al final de la página no hay nada de esto. Por eso el footer de cristal se probó y se retiró.

Mismo andamiaje.

El marcado
<div class="cristal p-5" style="--cristal-radio: var(--radius-xl); --cristal-brillo-ancho: 12rem; --cristal-brillo-alto: 6rem" data-cristal-luz>
  <span class="cristal__brillo" aria-hidden="true"></span>
  <p class="font-mono text-xs uppercase tracking-[0.18em] text-text-muted">Run activo</p>
  <p class="mt-2 text-sm text-text-secondary">El material no sabe si es una barra o un panel. Solo sabe que tiene algo detrás.</p>
</div>

Cuándo NO usarla

Es la parte que más valor tiene de esta página. Cada línea está aquí porque costó algo: se probó, se retiró, o se perdieron horas.

Clases

.cristal
El material. Radio de píldora por defecto.
.cristal__brillo
El brillo especular. Va en un elemento y no en un pseudo-elemento porque su posición la mueve el puntero por variable CSS, y porque el material deja ::before/::after libres para quien lo envuelva. Siempre aria-hidden="true".
.cristal--denso
Despegado del contenido: más tinta y sombra proyectada. Se lee como una capa que flota, no como una franja pegada. El velo no cambia de color al densificarse, solo de densidad: el cambio a fondo claro sobre un hero oscuro se percibe como un parpadeo.
.nav-bar / .nav-brillo / .is-scrolled
Alias heredados. El HTML de la landing usa esos nombres y no se toca. En código nuevo no se usan.

Mandos

Custom properties que se redefinen desde fuera sin tocar el archivo del sistema.

--cristal-radio var(--radius-full)
Píldora por defecto. Para una ficha o un dock, var(--radius-xl).
--cristal-desenfoque 20px
El radio del blur. El sistema no tiene escala de desenfoque; por eso es una perilla y no un token.
--cristal-saturacion 180%
El color de detrás se apaga al desenfocar; esto lo devuelve.
--cristal-brillo-ancho 18rem
El tamaño del reflejo. En una pieza pequeña hay que bajarlo o el brillo la cubre entera.
--cristal-brillo-alto 8rem
Ídem, en vertical.
--nx / --ny 18% 0%
La posición del brillo. Registradas con @property e inherits: true, así que se pueden escribir en el contenedor. Reposo en la esquina superior izquierda: sin guion la luz se queda quieta ahí y sigue leyéndose como reflejo, no como hueco.

Tokens de los que depende

Sin tokens.css importado antes, estas variables no existen: la pieza no falla, se pinta rota en silencio.

  • --color-bg-primary
  • --color-bg-surface-1
  • --color-bg-surface-2
  • --color-text-primary
  • --color-border
  • --color-border-hover
  • --color-shadow-ink
  • --radius-full
  • --dur-base
  • --ease-out-expo

Qué queda cuando algo falla

  • Sin guion del puntero la luz descansa en 18% 0% y sigue leyéndose como un reflejo. El guion son dos líneas que escriben --nx/--ny.
  • Sin backdrop-filter (@supports not) el velo cae a --color-bg-surface-1 sólido (-2 en denso). El filo se queda: el grosor del material no depende del desenfoque.
  • prefers-reduced-transparency: reduce apaga el desenfoque y vuelve el velo opaco. Quien pide menos transparencia está pidiendo poder leer, no un efecto degradado.
  • prefers-reduced-motion: reduce baja la transición a 0.01ms —no a none: con none algunos motores no disparan transitionend— y el guion del brillo se apaga solo bajo la misma consulta.

Accesibilidad

  • .cristal__brillo es aria-hidden="true": es material, no contenido.
  • El contenido va por encima del brillo por CSS (z-index: 1 a todo hijo que no sea el brillo). No hay que ordenarlo a mano.
  • El contraste del texto encima depende de lo que pase por detrás. Si el fondo puede ser claro, el velo denso no es opcional.

Lo que el sistema todavía no resuelve

Valores literales y huecos conocidos. Se escriben para que nadie los confunda con una decisión.

  • No hay token de sombra proyectada: el 0 12px 40px del denso se deriva de --color-bg-primary con color-mix. Funciona sobre #0b0b0c, pero si el lienzo se aclara la sombra se aclara con él.
  • Tampoco hay escala de desenfoque. Los 20px son una perilla local; si aparece un segundo cristal con otro blur, el sistema no tiene dónde arbitrar.