Skip to content
This page has been auto-translated and may contain errors.View in English

Propiedades personalizadas y CSS moderno

docs.scrimba.com

Escoges un azul de marca, lo escribes en cuarenta reglas diferentes y publicas el sitio. Un mes después marketing quiere un azul ligeramente diferente. Ahora estás buscando en cada hoja de estilos, esperando atrapar los cuarenta y no perder los dos que fueron escritos con mayúscula. Las propiedades personalizadas arreglan esto: nombras el color una sola vez, usas el nombre en todas partes, y lo cambias en un lugar. Ese único hábito es el comienzo de los temas, tokens de diseño, y hojas de estilos que sobreviven a un rediseño.

Declarar y usar una variable

Una propiedad personalizada es un valor al que le das un nombre para poder reutilizarlo. Escribes el nombre con dos guiones al frente, lo estableces a un valor, y luego lo lees de vuelta con var():

css
:root {
  --color-brand: #2563eb;   /* declara el valor una sola vez, bajo un nombre */
}

.button {
  background: var(--color-brand);   /* léelo de vuelta aquí */
}

.link {
  color: var(--color-brand);        /* y aquí, el mismo valor */
}

--color-brand es el nombre (los dos guiones son obligatorios), y var(--color-brand) es cómo lo usas. Declararlo en :root (que es el elemento <html>, la parte superior de la página) hace que el nombre esté disponible en todas partes. Cambia la línea única en :root y cada regla que la lee se actualiza.

Una propiedad personalizada es un valor nombrado que declaras con un prefijo -- y lees de vuelta con la función var(). La gente también las llama variables de CSS, y los dos nombres significan lo mismo:

css
:root {
  --color-brand: #2563eb;
  --color-brand-dark: #1e40af;
}

.button {
  background: var(--color-brand);
}

.button:hover {
  background: var(--color-brand-dark);
}

El prefijo -- es lo que separa una propiedad personalizada de una propiedad normal, y es sensible a mayúsculas y minúsculas, así que --color-Brand y --color-brand son dos nombres diferentes. Declarar en :root es la opción común porque pone el valor en la cima del árbol donde cada elemento puede verlo, que es donde pertenecen tus tokens globales. No estás limitado a colores: cualquier valor que una propiedad acepte puede vivir en una propiedad personalizada, incluyendo longitudes, sombras y pilas de fuentes.

Una propiedad personalizada es una propiedad definida por el usuario cuyo nombre comienza con --, que sostiene un valor que lees de vuelta a través de la función var(). El nombre es sensible a mayúsculas y minúsculas y el valor se almacena casi literalmente: el navegador hace muy poco con él en el momento de la declaración, lo cual se vuelve importante una vez que ves dónde realmente se resuelve.

css
:root {
  --color-brand: #2563eb;
  --space-md: 16px;
  --shadow-card: 0 1px 3px rgb(0 0 0 / 0.12);
}

.card {
  padding: var(--space-md);
  box-shadow: var(--shadow-card);
}

Declarar en :root es una convención, no un requisito. :root coincide con el elemento <html> con una especificidad ligeramente mayor que el selector de tipo html, y porque las propiedades personalizadas heredan, un valor establecido allí es visible para cada descendiente. Ese es el modelo mental a mantener: no estás creando un registro global, estás estableciendo una propiedad en el elemento raíz que resulta heredar el documento completo. Cualquier cosa que una propiedad normal acepte puede ser almacenada, y también almacenarás fragmentos de valores (un canal de color, un número sin unidades) y los ensamblarás después.

JunoDeclarar y usar una variable Una propiedad personalizada es un valor con un nombre: escríbelo con dos guiones como --color-brand, luego léelo con var(--color-brand). Ponlo en :root para que toda la página pueda usarlo. Cambia la línea única y cada regla que lee el nombre se actualiza con ella.
JunoDeclarar y usar una variable El prefijo -- marca una propiedad personalizada, y var() la lee de vuelta. Declarar en :root coloca tus tokens globales en la cima del árbol donde cada elemento puede alcanzarlos. Los nombres son sensibles a mayúsculas y minúsculas, y cualquier valor que una propiedad acepte puede vivir en uno, no solo colores.
JunoDeclarar y usar una variable Una propiedad personalizada es cualquier --nombre que estableces, lees de vuelta a través de var(), almacenado casi literalmente hasta que se resuelve. :root es una convención: es el elemento <html>, y porque estas propiedades heredan, un valor allí alcanza cada descendiente. También puedes almacenar valores parciales y ensamblarlos después, lo cual la sección de fallback comienza a usar.

En cascada y herencia

Si has usado variables en otro lenguaje, esta siguiente parte es la sorpresa: una propiedad personalizada no es fija. Sigue las mismas reglas que cualquier otra propiedad de CSS, lo que significa que hereda hacia elementos hijos, y puedes darle un valor diferente en un lugar sin tocar el resto.

css
:root {
  --color-text: #1f2937;   /* el predeterminado para toda la página */
}

.callout {
  --color-text: #92400e;   /* un valor diferente, solo dentro de .callout */
  color: var(--color-text);
}

p {
  color: var(--color-text);   /* gris oscuro normalmente, ámbar dentro de un callout */
}

El mismo var(--color-text) te da gris oscuro en la mayoría de párrafos pero ámbar dentro de un callout, porque el callout estableció su propio valor. Esto es lo que hace posible la temática, y has visto la cascada detrás de ello en cómo funciona CSS.

Aquí es donde una propiedad personalizada se separa de una variable de un preprocesador CSS como Sass. Una variable de preprocesador se compila antes de que el navegador la vea, así que es un buscar-y-reemplazar estático. Una propiedad personalizada es una propiedad CSS real, viva en el navegador, así que obedece la cascada e hereda.

El valor de una propiedad personalizada se busca en el elemento que la lee, no donde fue declarada. Ese hecho único es por qué funciona la temática de componentes:

css
:root {
  --color-accent: #2563eb;   /* azul en todas partes por defecto */
}

.card--warning {
  --color-accent: #d97706;   /* anulación para este subárbol de componentes */
}

.card__badge {
  background: var(--color-accent);   /* azul, o ámbar dentro de una tarjeta de advertencia */
}

La insignia no sabe ni le importa de dónde vino --color-accent. Lee el valor en efecto en sí misma, y cualquier ancestro podría haberlo redefinido. Anulas el token en un contenedor y cada descendiente que lo lea se rediseña, sin editar una sola regla de componente. Las reglas completas de cascada e herencia viven en cómo funciona CSS.

El comportamiento que separa las propiedades personalizadas de cada variable de preprocesador es que participan en la cascada e heredan por defecto. Una variable de Sass o Less se resuelve en tiempo de compilación y desaparece antes de que el navegador cargue el archivo. Una propiedad personalizada es una propiedad real en el elemento, resuelta por la misma cascada que rige color o font-size, lo que significa que var() se busca contra el valor computado de la propiedad en el elemento que hace la lectura.

css
:root {
  --color-accent: #2563eb;
}

.panel[data-tone="warning"] {
  --color-accent: #d97706;   /* establecido en la raíz del subárbol */
}

.panel__button {
  background: var(--color-accent);   /* resuelto por elemento, hereda del panel */
}

Dos consecuencias que valen la pena sostener. Primero, la herencia hace la temática: establecer --color-accent en una raíz de subárbol se propaga a cada descendiente que lo lee, así que una anulación de contenedor rediseña un árbol de componentes completo sin cambios a sus reglas. Segundo, la especificidad aún se aplica a dónde se establece el token, no a var() en sí. Si dos selectores ambos establecen --color-accent en el mismo elemento, la cascada usual decide cuál gana, exactamente como lo haría para cualquier propiedad. Una propiedad que debería dejar de heredar puede ser reiniciada con initial, o declarada como no heredable con @property, que la sección de profundidad más adelante alcanza. Las reglas de cascada se aplican aquí sin excepción.

JunoEn cascada y herencia Una propiedad personalizada no es congelada: hereda hacia elementos hijos, y puedes establecer un valor diferente en un lugar sin cambiar el resto. Por eso el mismo var(--color-text) puede ser gris en la mayoría de párrafos y ámbar dentro de un callout. Redefine el nombre en un contenedor y todo dentro de él recoge el nuevo valor.
JunoEn cascada y herencia A diferencia de una variable de Sass que desaparece en tiempo de compilación, una propiedad personalizada está viva en el navegador y sigue la cascada, así que su valor se lee en el elemento que la usa. Anula un token en un contenedor y cada descendiente se rediseña, con cero ediciones a las reglas de componentes. Ese comportamiento de buscar-donde-lees es el truco completo detrás de la temática.
JunoEn cascada y herencia Las propiedades personalizadas heredan y en cascada, lo que una variable de preprocesador nunca hace porque desaparece antes de que el navegador cargue. var() se resuelve contra el valor computado en el elemento que lee, así que establecer un token en una raíz de subárbol tematiza todo debajo a través de la herencia sola. La especificidad rige dónde se establece el token, no la llamada var() en sí.

Fallbacks y calc()

var() puede tomar un segundo valor, usado solo si la propiedad personalizada nunca fue establecida. Este es el fallback, y mantiene una regla funcionando incluso cuando un token falta:

css
.card {
  gap: var(--gap, 1rem);   /* usa --gap si existe, de lo contrario 1rem */
}

También puedes hacer matemáticas con una propiedad personalizada usando calc(). Si tienes un valor de espaciado base, puedes construir uno más grande a partir de él en lugar de codificar un segundo número:

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, derivado del base */
}

Derivar valores así significa que un cambio a --space-md se propaga a través de todo lo construido encima de él.

var() acepta un segundo argumento que actúa como un fallback: var(--gap, 1rem) se resuelve a --gap si se establece, y a 1rem si no. Ese fallback vale la pena usarlo en cualquier token que podría no ser definido en un subárbol dado, así un componente aún se renderiza sensatamente cuando se cae en algún lugar donde sus tokens no fueron proporcionados.

css
.card {
  gap: var(--gap, 1rem);
  padding: var(--space-md, 16px);
}

Las propiedades personalizadas llegan a su máxima expresión con calc(), porque así es cómo derivar un valor de otro en lugar de almacenar un montón de números mágicos:

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);        /* 32px */
  margin-bottom: calc(var(--space-md) * 3);  /* 48px, una única fuente de verdad */
}

Una cosa a vigilar: el fallback puede ser un valor completo incluyendo comas, así que var(--shadow, 0 1px 2px black) es válido, y todo después de la primera coma se trata como el fallback. Mira unidades de tamaño para cómo las unidades dentro de calc() se resuelven.

var() toma un fallback opcional como su segundo argumento: var(--gap, 1rem) cede 1rem cuando --gap no se establece en el elemento que lee (o se establece al valor garantizado-inválido). El fallback codiciosamente toma todo después de la primera coma, así que puede ser en sí mismo un valor que contiene comas o un var() anidado, lo que te permite encadenar predeterminados: var(--gap, var(--space-md, 1rem)).

css
.card {
  gap: var(--gap, var(--space-md, 1rem));   /* fallback encadenado */
}

:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, derivado no codificado */
}

El emparejamiento con calc() es donde las propiedades personalizadas se ganan su lugar en un sistema escalable, y viene con una regla de precisión. Una propiedad personalizada puede sostener un número sin unidades, y multiplicas una unidad en ella dentro de calc():

css
:root {
  --scale: 1.5;   /* sin unidades, así puede multiplicarse por cualquier unidad */
}

.title {
  font-size: calc(1rem * var(--scale));   /* 1.5rem */
  line-height: calc(var(--scale) + 0.2);  /* aritmética de número puro */
}

La razón por la que almacenas --scale sin una unidad es que calc(var(--scale) * 16px) funciona, mientras que almacenar --scale: 1.5rem e intentar calc(var(--scale) * 16px) multiplica dos longitudes y es inválido. Mantén proporciones crudas sin unidades y adjunta la unidad en el punto de uso. Cómo esas unidades se resuelven a píxeles se cubre en unidades de tamaño.

JunoFallbacks y calc()var(--gap, 1rem) usa el token si existe y cae a 1rem si no, así una regla sigue funcionando cuando un valor falta. Y calc() te permite construir valores desde un token, como calc(var(--space-md) * 2) para el doble del espaciado base. Cambia el base y todo construido en él sigue.
JunoFallbacks y calc() El segundo argumento para var() es un fallback: var(--gap, 1rem) cubre el caso donde el token nunca fue establecido. Empareja propiedades personalizadas con calc() para derivar valores en lugar de codificar números mágicos, así un token impulsa la escala completa. Nota el fallback se traga todo después de la primera coma, lo cual es útil para valores de múltiples partes.
JunoFallbacks y calc()var(--gap, 1rem) cae codiciosaemente, así puedes anidar predeterminados como var(--gap, var(--space-md, 1rem)). Almacena proporciones crudas sin unidades, porque calc(var(--scale) * 16px) funciona mientras que multiplicar dos longitudes no, y adjunta la unidad en el punto de uso. Esto mantiene un token impulsando una escala de tipo o espaciado completa.

Temática

Porque una propiedad personalizada puede ser redefinida más abajo del árbol, puedes construir un tema oscuro cambiando los tokens bajo una clase, y dejar cada regla de componente sola:

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
}

.theme-dark {
  --color-bg: #0f172a;      /* los mismos nombres, nuevos valores */
  --color-text: #e2e8f0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
}

Añade class="theme-dark" a la página y cada regla que lee --color-bg o --color-text cambia a los valores oscuros. Nunca tocaste la regla body, o cualquier componente, para hacer el cambio de tema. Ese es el beneficio de nombrar tus colores como tokens en lugar de escribir los códigos hex directamente.

La temática es la recompensa directa de tokens que en cascada. Defines tus colores una sola vez como tokens, escribes cada componente contra los nombres de tokens, y entonces un tema no es nada más que un segundo conjunto de valores para esos mismos nombres bajo una clase o una consulta de medios:

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

Alternar theme-dark en un contenedor rediseña todo dentro de él, porque los componentes leen nombres de tokens, no colores fijos. Lo mismo funciona con una consulta de medios así el tema sigue la configuración del sistema operativo:

css
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

La disciplina que hace esto mantenerse es mantener códigos hex crudos fuera de reglas de componentes completamente, y nombrarlos como tokens de color en lugar. Esto es también donde una convención de nombrado de tokens paga, que organizar CSS entra.

La temática es la razón por la que las propiedades personalizadas importan para cualquier cosa más allá de una reutilización de valor único, y funciona porque un tema no es nada más que una anulación de cascada de tu capa de tokens. Los componentes leen nombres de tokens; un tema redefine esos nombres en un punto más alto del árbol. Nada en la capa de componentes sabe que un tema existe.

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

@media (prefers-color-scheme: dark) {
  :root:not(.theme-light) {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

El patrón que se mantiene en producción es un sistema de tokens de dos capas. Una primera capa sostiene valores crudos (--blue-600: #2563eb), y una segunda capa sostiene tokens semánticos que los referencian (--color-accent: var(--blue-600)). Los componentes leen solo la capa semántica, así un tema reasigna los tokens semánticos y la capa de paleta permanece puesta, lo que mantiene el cambio pequeño y la intención legible. Emparejar una anulación de clase con la consulta de medios prefers-color-scheme, como arriba, te da un predeterminado de sistema más un alternar manual sin duplicar reglas de componentes. La disciplina de nombrado que mantiene esto mantenible pertenece con el resto de tu organización de archivos y tokens, y los valores crudos en sí mismos son colores.

Tres cosas más pertenecen en una vista avanzada de propiedades personalizadas, porque cambian lo que es posible en lugar de añadir pulido.

Las propiedades personalizadas se resuelven en tiempo de valor computado, lo que significa que el navegador resuelve var() por elemento conforme computa estilos, no cuando el archivo se analiza. Esto es exactamente por qué la temática funciona y por qué una variable de preprocesador no puede hacerlo: el valor está vivo, así que responde a la cascada, a la herencia, y a cambios hechos en tiempo de ejecución.

El tiempo de ejecución es el segundo punto. Porque estas son propiedades reales, JavaScript puede leerlas y escribirlas, y cada regla leyendo el token se actualiza instantáneamente:

css
:root {
  --sidebar-width: 280px;   /* leído y establecido desde JS en tiempo de ejecución */
}

Un script puede llamar a document.documentElement.style.setProperty('--sidebar-width', '320px') y el diseño se refluye contra el nuevo valor, algo que una variable de Sass, compilada lejos mucho antes de que la página cargue, nunca puede ofrecer. También puedes delimitar esto a un subárbol estableciendo la propiedad en cualquier elemento en lugar de la raíz, que confina un cambio a esa rama del árbol.

El tercer punto es el control sobre la propiedad en sí. @property te deja registrar una propiedad personalizada con un tipo, un valor inicial, e si hereda, lo que desbloquea animación suave de propiedades personalizadas (una propiedad personalizada simple anima como un salto encendido/apagado porque el navegador no puede interpolar un valor sin tipo). Junto a ello, las funciones de valor moderno leen naturalmente desde tokens: clamp(min, preferred, max) escoge el valor preferido pero nunca deja los límites, min() retorna el más pequeño de sus argumentos y max() el más grande, así width: min(100%, var(--max-width)) tapa un ancho fluido a un token sin una consulta de medios.

JunoTemática Un tema es los mismos nombres de tokens con valores diferentes bajo una clase como .theme-dark. Establece class="theme-dark" y cada regla leyendo esos tokens cambia, sin editar un sólo componente. Por eso nombras tus colores como tokens en lugar de escribir el código hex directamente en cada regla.
JunoTemática Porque los tokens en cascada, un tema es solo un segundo conjunto de valores para los mismos nombres, bajo una clase o una consulta de medios prefers-color-scheme. Mantén hex crudos fuera de reglas de componentes y lee nombres de tokens en lugar, y un subárbol completo se rediseña desde una anulación. Alterna la clase o empareja la configuración del SO y todo leyendo esos tokens sigue.
JunoTemática Las propiedades personalizadas se resuelven en tiempo de valor computado, así que están vivas: JavaScript puede establecer --sidebar-width en tiempo de ejecución y cada regla leyéndola se refluye, lo que ninguna variable de preprocesador puede hacer. Una configuración de dos capas, tokens de paleta crudos bajo tokens semánticos, mantiene un cambio de tema a un puñado de reasignaciones. Y @property más clamp(), min(), y max() dejan esos tokens animar y permanecer acotados sin una consulta de medios.