Atributos dinámicos y class:list
Interpolar valores en atributos sin comillas, esparcir objetos de atributos con la sintaxis de propagacion, componer clases condicionales con la directiva class:list, y la logica de presencia de los atributos booleanos de HTML frente a los aria.
Interpolar texto es media historia; la otra media son los atributos. Astro deja poner expresiones en cualquier atributo, esparcir objetos enteros de una vez y componer clases condicionales con una directiva dedicada. Y respeta una sutileza que muchos motores de plantillas ignoran: los atributos booleanos de HTML se rigen por presencia, no por valor. Dominar esto es dominar la plataforma, no un dialecto de Astro.
- Interpolar expresiones en atributos y esparcir objetos con la sintaxis de propagación.
- Componer clases condicionales con la directiva
class:listy sus formas de entrada. - Entender los atributos booleanos: presencia frente a ausencia, no
truefrente afalse. - Distinguir un booleano de HTML real de un atributo
aria-, que sí lleva valor de texto.
Atributos dinámicos
Cualquier atributo acepta una expresión entre llaves, y —detalle importante— sin comillas alrededor. Se escribe href={url}, no href="{url}": esto último pondría la cadena literal con las llaves incluidas. El valor se calcula en el servidor y se escapa igual que el texto, así que interpolar en un atributo es tan seguro como interpolar en el cuerpo.
---
const enlace = { url: '/docs', texto: 'Documentación' };
---
<a href={enlace.url} title={enlace.texto}>{enlace.texto}</a>
Cuando ya tienes un objeto de atributos, la sintaxis de propagación los vuelca todos de golpe. Es el patrón para reenviar props recibidas a un elemento nativo sin listarlas una a una, algo habitual al construir componentes que envuelven etiquetas del DOM.
---
const attrs = { id: 'cta', 'data-track': 'hero', role: 'button' };
---
<div {...attrs}>Llamada a la acción</div>
Y del mismo modo que en el texto, un valor undefined o null en un atributo hace que Astro lo omita por completo, en vez de escribir un valor basura como alt="undefined". Es coherente con lo que verás en los booleanos: ante la duda, Astro prefiere la ausencia de un atributo a un valor sin sentido. Esa regla convierte los atributos opcionales en algo trivial —pasas undefined y el atributo desaparece—, sin ternarios que decidan entre ponerlo o no.
---
const textoAlt = undefined;
---
<img src="/logo.svg" alt={textoAlt} />
<!-- salida: <img src="/logo.svg"> sin ningun atributo alt -->
Cuando mezclas la propagación con atributos escritos a mano, gana el último en aparecer. En <div {...attrs} class="fijo">, el class explícito pisa al que trajera attrs; si inviertes el orden, es attrs quien manda. Es la misma regla de sobrescritura de un objeto de JavaScript, aplicada a los atributos: coloca a la derecha lo que quieras que prevalezca.
class:list: componer clases condicionales
Construir una cadena de clases a mano —concatenando con espacios y ternarios— es frágil: sobran o faltan espacios, se cuela un undefined, se duplican clases. class:list es la directiva que compone la clase final a partir de varias fuentes y limpia el resultado. Acepta una mezcla de formas:
- cadenas sueltas, como
'boton', que se incluyen tal cual; - objetos, donde la clave se incluye solo si su valor es verdadero:
{ activo: esActivo }; - arrays, que se aplanan y se recorren en profundidad;
- un
Set, que además deduplica de forma natural;
y descarta cualquier valor falsy que encuentre por el camino.
---
const esActivo = true;
const deshabilitado = false;
---
<button
class:list={[
'boton',
{ activo: esActivo, deshabilitado },
esActivo && 'resaltado',
]}
>
Guardar
</button>
<!-- salida: <button class="boton activo resaltado">Guardar</button> -->
El resultado se resuelve a un atributo class corriente, y si la lista queda vacía Astro no emite ningún atributo class. Es el equivalente integrado de librerías como clsx, sin instalar nada. La distinción práctica es clara: para elegir entre dos cadenas basta un class={cond ? 'a' : 'b'} y se lee bien; reserva class:list para cuando sumas condiciones independientes que pueden coincidir o no. No necesitas ambos en el mismo elemento, porque class:list ya admite una cadena estática como una entrada más del array.
De todas las entradas que acepta class:list, la forma objeto —{ activo: esActivo, error: hayError }— es la que más rinde, porque empareja cada clase con su condición en el mismo sitio. Se lee como una tabla: a la izquierda la clase, a la derecha cuándo aparece. Reserva las cadenas sueltas para las clases fijas y deja que el objeto exprese lo condicional; el array de nivel superior solo une ambas familias.
Atributos booleanos: presencia, no valor
HTML tiene una familia de atributos —disabled, checked, required, hidden, readonly, selected— cuya semántica es binaria y peculiar: lo que cuenta es que estén o no, no su valor. Un disabled="false" sigue deshabilitando el botón, porque el navegador solo mira si el atributo existe. Un motor ingenuo que escriba literalmente disabled="false" produce el bug clásico: el elemento queda deshabilitado justo cuando querías lo contrario.
Astro conoce ese conjunto y hace lo correcto: mapea un booleano de JavaScript a presencia o ausencia. <button disabled={estaOcupado}> emite el atributo disabled cuando estaOcupado es verdadero y lo omite por completo cuando es falso —no disabled="false", sino nada—.
---
const estaOcupado = false;
const esRequerido = true;
---
<button disabled={estaOcupado}>Enviar</button>
<input required={esRequerido} />
<!-- salida: <button>Enviar</button> y <input required> -->
Esa lista de atributos booleanos no la inventa Astro: la fija la especificación de HTML, y Astro la conoce de antemano. Por eso no tienes que declararle cuáles tratar así; reconoce disabled, checked, multiple, open, autoplay y toda la familia, y les aplica la regla de presencia por igual. Tu trabajo se reduce a pasar un booleano y confiar en que el marcado saldrá correcto.
Cuidado, eso sí, con confundir estos booleanos del estándar con los atributos aria-, que no son booleanos de HTML: toman una cadena de texto, y aria-hidden="false" significa algo distinto de omitir el atributo. Por eso Astro renderiza aria-expanded={false} como aria-expanded="false", con su valor de texto, y no lo borra. La regla mental que evita el error: los booleanos reales del estándar se rigen por presencia; los aria- y los data- siempre llevan valor.
flowchart LR S[cadena suelta] --> R[la directiva resuelve] O[objeto condicional] --> R A[array anidado] --> R R --> F[descarta falsy y deduplica] F --> C[atributo class limpio] style S fill:#89b4fa,color:#11111b style C fill:#a6e3a1,color:#11111b
La lección sutil de los atributos es filosófica antes que técnica: Astro elige aumentar HTML en lugar de sustituirlo. Un framework de cliente construye su propio modelo de atributos —un sistema de propiedades que el runtime traduce al DOM en cada render— y esa capa intermedia se convierte en la verdad con la que trabajas, a veces alejada de cómo funciona de verdad la plataforma. Astro va en dirección contraria: sus atributos dinámicos son el modelo real de HTML con una fina piel de JavaScript encima. Por eso respeta que los atributos booleanos se rigen por presencia y no por valor, hasta el punto de omitir por completo un disabled falso en lugar de escribir el engañoso disabled="false"; por eso distingue un booleano del estándar de un aria- que lleva texto; por eso class:list, con toda su comodidad, no es más que azúcar que se resuelve a un atributo class corriente. No hay un DOM virtual que interprete tus intenciones: hay una función que corre en el servidor, calcula cadenas y presencias, y las hornea en un HTML que ya es definitivo. Esa fidelidad tiene un precio y una recompensa. El precio es que no hay atadura viva: cambiar una variable después del build no mueve un atributo, porque el atributo ya es texto congelado, y para eso necesitas JavaScript de cliente. La recompensa es que lo que aprendes es HTML, no un dialecto que solo existe dentro de un framework. Cuando dominas los atributos en Astro, no dominas una abstracción de Astro: dominas la plataforma, con Astro apartándose para dejártela ver tal cual es, solo que ahora calculable desde el servidor.
- Interpola una expresión en
hrefy entitle, y comprueba que el valor va sin comillas alrededor de la llave. - Reenvía un objeto de atributos a un elemento con la sintaxis de propagación
{...attrs}y revisa el HTML resultante. - Compón tres fuentes de clases con
class:list—una cadena, un objeto condicional y un&&— y observa en la salida cómo se descartan las entradas falsy. - Pon
disabled={false}en un botón y confirma en el HTML que el atributo no aparece; luego pruebaaria-disabled={false}y anota por qué este sí conserva su valor.