wandres.dev
EVENTOS Y BINDING · on:, delegación, refs

classList y style reactivos

Alternar clases con un objeto en classList, estilos dinámicos con objetos de propiedades CSS y variables, los espacios de nombres finos class: y style:, y el modelo de coste real: qué efecto se crea y cuántas escrituras al DOM produce cada forma.

⏱ 13 min

Cambiar el aspecto de un nodo según el estado es el pan de cada día, y Solid ofrece cuatro formas de hacerlo: classList con un objeto, style con un objeto, y los espacios de nombres finos class: y style:. No son intercambiables: cada una crea un número distinto de efectos reactivos y produce un número distinto de escrituras al DOM. Elegir bien es la diferencia entre una animación fluida y una que reflow constantemente.

🎯 Al terminar esta lección sabrás
  • Alternar varias clases con el objeto de classList y entender su diffing.
  • Escribir estilos dinámicos con el objeto de style, nombres CSS y variables.
  • Usar los espacios finos class:nombre y style:propiedad para máxima granularidad.
  • Razonar el coste: cuántos efectos y cuántas escrituras al DOM genera cada forma.

classList: alternar clases por objeto

Cuando una clase depende del estado, la tentación es concatenar strings. Solid ofrece algo mejor: classList toma un objeto donde las claves son nombres de clase y los valores son booleanos (o accesores que devuelven booleanos).

function Pestana(props: { activa: boolean; deshabilitada: boolean }) {
  return (
    <button
      class="pestana"                          // clase base estatica
      classList={{
        "pestana--activa": props.activa,        // se anade o quita segun el booleano
        "pestana--off": props.deshabilitada,
        "pestana--activa pestana--grande": props.activa,  // varias clases por clave
      }}
    >
      {props.children}
    </button>
  );
}

Solid compila classList a un efecto que diferencia el objeto nuevo contra el anterior. Solo toca el DOM para las clases que cambiaron de estado: si activa pasa de true a false pero deshabilitada no cambia, únicamente se elimina pestana--activa, sin tocar el resto. Una clave puede contener varias clases separadas por espacio, y Solid las gestiona todas juntas.

💡
class base estatica, classList para alternar

El patrón idiomático es una class estática con las clases que siempre están, y classList (o class:) para las que se alternan. Evita poner una class reactiva que cambie: al reescribir el atributo entero puede pisar lo que gestiona classList. Base fija en class, dinámica en classList, y nunca la misma clase en ambos sitios.

style: objetos, nombres CSS y variables

El binding style acepta un objeto, pero con dos diferencias cruciales respecto a React. Primero, las claves son nombres CSS reales en kebab-case, no camelCase: se escribe background-color, no backgroundColor. Segundo, Solid usa element.style.setProperty, lo que te habilita las variables CSS de forma nativa y reactiva.

function Barra(props: { progreso: number; color: string }) {
  return (
    <div
      class="barra"
      style={{
        width: `${props.progreso}%`,       // valor numerico dinamico, con unidad
        "background-color": props.color,     // nombre CSS en kebab-case
        "--brillo": props.progreso > 80 ? "1.2" : "1", // variable CSS reactiva
      }}
    />
  );
}

Igual que classList, el style de objeto se compila a un efecto que lee todas las propiedades. Cuando cualquiera de ellas cambia, el efecto se re-ejecuta, reconstruye el objeto y hace un diff contra el anterior, aplicando setProperty (o removeProperty) solo a las claves que de verdad cambiaron. Un string en style también vale, pero entonces cada cambio reescribe el atributo completo: pierdes el diffing.

Las variables CSS reactivas brillan precisamente aquí: expones un -- en un contenedor y dejas que la hoja de estilos lo consuma, actualizando muchos nodos con una sola escritura.

<section style={{ "--acento": tema().acento }}>
  {/* cualquier regla que use var(--acento) reacciona sin tocar cada nodo */}
</section>

Los espacios finos: class: y style:

Para la máxima granularidad reactiva existen dos espacios de nombres que enlazan una sola clase o propiedad:

<div
  class:activa={esActiva()}          // un efecto propio, solo depende de esActiva
  class:cargando={estaCargando()}    // otro efecto independiente
  style:color={color()}              // un efecto por propiedad
  style:transform={`translateX(${x()}px)`}
/>

La diferencia con las formas de objeto es de granularidad de seguimiento. classList={{a, b}} crea un efecto que depende de a y de b: si cambia a, el efecto entero se re-ejecuta y re-diffea ambas. class:a y class:b crean dos efectos independientes: cambiar a solo re-ejecuta el suyo. En la escritura al DOM ambas son mínimas; la diferencia está en cuánto trabajo de recomputación desencadena cada cambio.

Un límite decide por ti cuál usar: class: y style: exigen un nombre estático —es parte de la sintaxis del binding—, así que para nombres calculados en runtime necesitas la forma de objeto con clave dinámica:

<div class:activa={on()} />                      // nombre fijo, efecto propio
<div classList={{ [claseDinamica()]: on() }} />   // nombre calculado en runtime
<div style={{ [propCSS()]: valor() }} />          // idem para propiedades
flowchart TD
V[Valor reactivo de estilo] --> O{Como lo enlazas}
O -->|objeto classList| P[Un efecto que diffea todas las clases juntas]
O -->|objeto style| Q[Un efecto que diffea todas las propiedades]
O -->|namespace class| R[Un efecto propio por cada clase]
O -->|namespace style| S[Un efecto propio por cada propiedad]
P --> W[Escrituras al DOM minimas solo lo que cambio]
Q --> W
R --> W
S --> W
style R fill:#a6e3a1,color:#11111b
style S fill:#a6e3a1,color:#11111b
style P fill:#89b4fa,color:#11111b
style Q fill:#89b4fa,color:#11111b

El coste real y cuándo importa

Todas estas formas ya hacen lo correcto: escrituras mínimas al DOM. La optimización de la que se habla menos no es cuántas propiedades escribes, sino cuánto reflow provocas. Cambiar clases que solo afectan a color u opacity es barato; cambiar width, top o cualquier propiedad de layout fuerza al navegador a recalcular la geometría de la página.

El caso legítimo del style fino es justo ese valor continuo que no conoces de antemano: una posición que sigue al puntero, animada con transform para que la GPU la componga sin tocar el layout.

import { createSignal } from "solid-js";

function Halo() {
  const [pos, setPos] = createSignal({ x: 0, y: 0 });
  return (
    <div
      class="halo"
      style:transform={`translate(${pos().x}px, ${pos().y}px)`} // solo composite
      onPointerMove={(e) => setPos({ x: e.clientX, y: e.clientY })}
    />
  );
}
📝
Estilos inline solo para lo verdaderamente dinámico

Reserva el style inline para valores continuos que no puedes conocer de antemano: una posición calculada, un transform que sigue al ratón, una altura medida. Para estados discretos y conocidos (activo, error, seleccionado) prefiere alternar clases: el navegador cachea mejor las reglas CSS, la especificidad es más manejable y separas presentación de lógica. Y para animar, quédate en transform y opacity, que la GPU compone sin tocar el layout, en vez de animar width o left.

Reactividad fina de un lado, DOM mínimo del otro

La lección profunda es que en Solid hay dos granularidades distintas y no debes confundirlas. Está la granularidad reactiva —cuántos efectos se crean y qué re-ejecuta cada cambio— y está la granularidad de escritura al DOM —cuántos setProperty acaban ocurriendo—. Las formas de objeto, classList y style, agrupan varias propiedades bajo un solo efecto: un cambio en cualquiera re-corre el diff de todas, pero solo escribe lo que cambió. Los espacios finos, class: y style:, dan un efecto por binding: recomputación aislada, a cambio de más nodos en el grafo. Ninguna es universalmente mejor. Con dos o tres propiedades que suelen cambiar juntas, el objeto es más legible y su coste de re-diff es trivial. Con una propiedad que cambia a 60 fps mientras otras diez permanecen quietas, el espacio fino evita re-evaluar las diez en cada frame. Pensar así —separar el grafo reactivo de las mutaciones del DOM, y ambos del coste de reflow del navegador— es lo que distingue un ajuste de rendimiento con criterio de un cambio a ciegas.

⚔️ Compara granularidades
  1. Crea un componente con un objeto classList de tres estados y comprueba en las DevTools que al alternar uno solo se añade o quita esa clase.
  2. Reescríbelo con tres class: independientes y observa que el resultado en el DOM es idéntico.
  3. Anima una barra con style:transform={translateX(...)} a 60 fps y compárala con animar style:left: mide el reflow en el panel de rendimiento.
  4. Define una variable CSS --acento reactiva con style y úsala desde tu hoja de estilos; cámbiala y verifica que todo lo que la consume se actualiza sin tocar cada nodo.
  5. Provoca a propósito el antipatrón: una class reactiva más un classList con una clase repetida, y observa cómo se pisan.