Variables CSS y define:vars
El puente entre el frontmatter y la hoja de estilos: las propiedades personalizadas de CSS como vocabulario compartido, la directiva define:vars para inyectar valores dinámicos desde el componente, su mecánica interna de declaración en línea sin hoisting, y el reparto sensato entre tokens estáticos y valores calculados.
El CSS de un componente se compila y se agrupa en tiempo de build, mientras que su frontmatter es JavaScript que produce valores en cada render. ¿Cómo cruza un dato de un mundo al otro, de una prop calculada a una regla de estilo? La respuesta tiene dos piezas: las propiedades personalizadas de CSS, que actúan de puente, y la directiva define:vars, que las tiende automáticamente desde el frontmatter hacia el bloque de estilos.
- Usar propiedades personalizadas de CSS como vocabulario compartido entre componentes.
- Inyectar valores del frontmatter en el CSS con la directiva
define:vars. - Explicar por qué un bloque con
define:varsse declara en línea y no se agrupa. - Repartir con criterio los tokens estáticos y los valores dinámicos calculados por render.
Las variables CSS como puente entre dos mundos
Una propiedad personalizada —--acento: #7c3aed— es una variable que vive dentro del propio CSS. Se declara en un selector, se hereda por la cascada y se lee con var(--acento). A diferencia de las variables de un preprocesador, que se resuelven en build y desaparecen, estas existen en runtime: el navegador las conoce, puedes cambiarlas con JavaScript y responden a la cascada como cualquier otra propiedad.
:root { --acento: #7c3aed; --radio: 8px; }
.cta { background: var(--acento); border-radius: var(--radio); }
Esa persistencia es lo que las convierte en puente. Cuando declaras tus tokens de diseño en :root dentro de un global.css, creas un vocabulario que cualquier componente consume sin importar nada: un idioma común de colores, espaciados y radios. Pero ese puente, tendido a mano, solo transporta valores fijos. Para cruzar un dato que el componente calcula en su frontmatter, necesitas que Astro tienda el puente por ti.
Antes de automatizar ese puente, subraya una ventaja de que estas variables vivan en runtime: puedes cambiarlas después de cargar la página. Un solo gesto de JavaScript reasigna un token y, con él, todo lo que lo consume se repinta sin tocar una sola regla:
// cambiar un token en runtime repinta todo lo que depende de el
document.documentElement.style.setProperty('--acento', '#0ea5e9');
Esta es la base de los temas conmutables —claro y oscuro, por ejemplo—: una variable en :root que cambia y arrastra consigo la interfaz entera. La propiedad personalizada no es solo un puente estático, sino un punto de control vivo que el resto del CSS obedece.
define:vars: pasar el frontmatter al CSS
La directiva define:vars se coloca en un bloque <style> y recibe un objeto. Astro toma cada clave de ese objeto, la convierte en una propiedad personalizada homónima y la pone a disposición del bloque. Dentro del CSS la lees con var(--clave). Así, un color o un tamaño calculados en el frontmatter aterrizan en las reglas sin que escribas una sola línea de pegamento.
---
interface Props { acento: string; radio: number; }
const { acento, radio } = Astro.props;
---
<button class="cta">Comprar</button>
<style define:vars={{ acento, radio: `${radio}px` }}>
.cta {
background: var(--acento);
border-radius: var(--radio);
color: white;
padding: 0.6rem 1.2rem;
}
</style>
La correspondencia es literal: la clave acento se vuelve --acento, y la clave radio se vuelve --radio, sin transformaciones de mayúsculas ni conversiones ocultas. Cada instancia del componente puede recibir props distintas y, por tanto, pintar con valores distintos, aunque todas compartan exactamente el mismo CSS. El estilo es uno; los datos, muchos.
Por eso la firma del componente gana en expresividad: la apariencia deja de estar cableada en el CSS y pasa a formar parte del contrato de props. Quien use el botón decide su acento al invocarlo, igual que decidiría su texto, sin abrir el bloque de estilos ni conocer sus interioridades. La configurabilidad visual se vuelve parte de la interfaz pública del componente.
define:vars no se limita a colores y tamaños. Admite cualquier valor serializable: cadenas, números, incluso el resultado de un cálculo hecho en el frontmatter. Lo habitual es pasar dimensiones y colores ya formateados con su unidad —de ahí el ${radio}px del ejemplo—, para que el CSS los reciba listos para usar. La regla mental es sencilla: si puedes escribirlo como texto, puede viajar como variable.
Qué ocurre por dentro (y qué implica)
Aquí aparece la consecuencia que distingue a un usuario avanzado. Como los valores de define:vars se calculan en el componente y pueden variar en cada render, Astro no puede agrupar ni compartir ese bloque como hace con los estilos normales. En su lugar, declara las propiedades personalizadas en línea, ancladas al ámbito del componente, y renderiza ese <style> junto al marcado en vez de izarlo a una hoja común. El bloque con define:vars queda, por diseño, fuera del mecanismo de hoisting.
En el HTML se ve sin ambigüedad: las variables aterrizan en el atributo style del propio elemento, mientras la regla que las usa vive, agrupada, en la hoja común.
<!-- la variable viaja en linea; la regla que la consume, no -->
<button class="cta" data-astro-cid-x style="--acento:#7c3aed;--radio:8px">
Comprar
</button>
flowchart LR FM[valor en el frontmatter] --> DV[directiva define vars] DV --> CP[propiedad personalizada en linea] CP --> SCOPE[declarada en el ambito del componente] SCOPE --> VAR[var referencia el valor en el CSS] VAR --> OUT[estilo con datos dinamicos] style FM fill:#89b4fa,color:#11111b style OUT fill:#a6e3a1,color:#11111b
De ahí una guía de higiene: mantén el bloque define:vars pequeño. Si un componente aparece cien veces, cien copias de un <style> gigante viajarán inline en el HTML, porque ninguna se puede deduplicar. La técnica correcta es separar responsabilidades: las reglas estáticas —lo que no cambia entre instancias— van en un <style> normal que Astro agrupa una sola vez, y solo las pocas líneas que consumen los valores dinámicos van en el bloque con define:vars. Así pagas inline únicamente por lo que de verdad varía.
La técnica en código se ve así: un <style> normal para lo estático —que Astro agrupa una sola vez— y un segundo bloque con define:vars, mínimo, solo con las reglas que consumen los valores dinámicos.
<style>
.cta { padding: 0.6rem 1.2rem; font-weight: 600; color: white; }
</style>
<style define:vars={{ acento }}>
.cta { background: var(--acento); }
</style>
El primer bloque se comparte entre todas las instancias; el segundo, diminuto, es el único que viaja en línea. Repartir así el CSS es la diferencia entre un HTML esbelto y uno inflado por la duplicación.
Es un error frecuente y silencioso: volcar todo el CSS del componente en el bloque con define:vars porque una sola regla necesita un valor dinámico. El resultado es que un montón de reglas que podrían agruparse se duplican en cada instancia. Divide el estilo en dos bloques —uno estático agrupable, otro dinámico en línea— y deja que solo lo imprescindible pague el precio de no agruparse.
Tokens estáticos frente a valores dinámicos
Con las piezas sobre la mesa, el reparto se vuelve claro. Cada valor de tu diseño encaja en una de tres categorías, y elegir bien la vía mantiene el CSS ligero y el sistema comprensible.
Token estático en :root
Colores de marca, escala de espaciado, radios. No dependen del render. Viven en global.css y se agrupan una vez para todo el sitio.
Valor dinámico con define:vars
Un color de acento por prop, una altura calculada, un porcentaje de progreso. Cambian por instancia y se inyectan en línea.
Puente hacia el script
define:vars también funciona en un <script>, serializando datos del servidor para que el JavaScript del cliente los lea.
La tercera vía merece un ejemplo, porque cierra el círculo: la misma directiva que lleva datos al CSS puede llevarlos al cliente. En un <script>, define:vars serializa el objeto y lo hace disponible como variables en el ámbito del script, un canal limpio para pasar del servidor al navegador sin incrustar JSON a mano en el marcado.
---
const usuario = { nombre: 'Ada', racha: 7 };
---
<script define:vars={{ usuario }}>
console.log(`Hola ${usuario.nombre}, racha de ${usuario.racha}`);
</script>
Una última precisión sobre el <script>: como los valores se serializan a JSON, define:vars transporta bien datos —objetos planos, cifras, cadenas—, pero no funciones ni instancias con comportamiento. Es un canal para datos, no para lógica.
Lo que cruza por define:vars, tanto hacia el CSS como hacia el script, debe poder representarse como texto. Un color, un número o un objeto plano viajan sin problema; una función, una fecha con métodos o una referencia al DOM, no. Si necesitas comportamiento en el cliente, pasa los datos por define:vars y escribe la lógica dentro del propio script.
La lección honda de define:vars es que no es un mecanismo de estilo, sino un mecanismo de datos que aprovecha el mejor conducto que el navegador ofrece para ellos: la propiedad personalizada. Conviene ver por qué esa elección es tan afortunada. El frontmatter de Astro corre en el servidor, en build o por petición; el CSS se aplica en el cliente. Entre ambos hay un abismo temporal y de entorno, y la tentación ingenua sería salvarlo generando CSS distinto para cada valor —una regla por instancia, con el color ya incrustado—. Eso destruiría el hoisting, inflaría el HTML y acoplaría la estructura del estilo a los datos. Astro rehúsa ese camino. En lugar de variar las reglas, mantiene las reglas fijas y varía solo las variables que consumen: la regla background: var(--acento) es una y la misma para las cien instancias, y lo único que cambia, inline y minúsculo, es la declaración de --acento. La propiedad personalizada actúa así de junta articulada entre un CSS estático, compartible y agrupable, y un dato dinámico, local y ligero. Y como la misma directiva sirve para el <script>, el patrón se generaliza: define:vars es, en el fondo, el puente canónico de Astro desde el frontmatter hacia el runtime, ya sea el runtime del motor de estilos o el del JavaScript del cliente. Interiorizar esto cambia tu forma de diseñar: dejas de preguntarte cómo generar el estilo correcto y empiezas a preguntarte qué dato debe cruzar el puente, confiando en que la estructura permanezca fija mientras solo el valor viaja.
- Declara un juego de tokens en
:rootdentro de tuglobal.cssy consúmelos convar(...)desde varios componentes. - Crea un componente que reciba un color por prop y píntalo con
define:vars; úsalo tres veces con colores distintos y comprueba que comparten CSS. - Divide ese componente en dos bloques
<style>, uno estático agrupable y otro dinámico en línea, e inspecciona el HTML para ver la diferencia. - Usa
define:varsen un<script>para pasar un dato del frontmatter al cliente y léelo desde la consola del navegador.