Herramientas de usuario

Herramientas del sitio


clase:daw:diw:sistema-diseno-tokens

13. Sistema de diseño: Tokens

En el tema de BEM los componentes tenían los valores puestos directamente. En este tema vamos a hacer bien un componente usando tokens, que es lo mismo que hace Angular Material pero hecho por nosotros:

  • Tokens de sistema: Variables CSS globales que usan todos los componentes. En nuestro sistema empiezan por --mlt-sys- (como --mat-sys- en Angular Material).
  • Tokens de diseño: Variables CSS públicas de cada componente, por ejemplo --boton-color-peligrosa (como --mat-button-filled-container-color en Angular Material). Su valor por defecto es un token de sistema.

Además, cada componente tiene un estado privado: variables que no son tokens sino CSS que necesita el componente para funcionar y que no le interesan al diseñador.

Números mágicos

Veamos el estilo del botón que hicimos en Angular: Creación de componentes (boton.scss):

.boton {
  font-family: sans-serif;
  font-size: 16px;
  padding: 6px;
 
  border-radius: 6px;
  border-width: 1px;
  border-style: solid;
 
  display: inline-block;
  cursor: pointer;
  text-decoration: none;
 
 
  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}

Desgraciadamente tiene muchos números mágicos, es decir números puestos directamente. Para evitarlo se usan variables CSS (cómo se crean y se usan está en CSS: Básico, «Variables CSS»): los tokens.

Tokens de sistema

Los tokens de sistema son variables CSS globales con todos los valores que se pueden usar: colores, fuentes, tamaños, márgenes, etc. Así limitamos las opciones a una serie de valores, como vimos en el tema de diseño.

A las variables del sistema les ponemos el prefijo mlt (de Mislata) de la misma forma que mat es de Angular Material.

Y añadimos sys al igual que hace Angular Material para decir que son variable que se usan en todo el sistema ya que afectan a todos los componentes.

  • --mlt-sys-color-alternativo-5: En nuestro framework de clase en Mislata
  • --mat-sys-secondary: En Angular Material

Los 9 tonos de cada color se han generado con el Color Scheme Generator:

:root {
  --mlt-sys-color-principal-1: #011019;
  --mlt-sys-color-principal-2: #052F4D;
  --mlt-sys-color-principal-3: #0B4D83;
  --mlt-sys-color-principal-4: #1363B4;
  --mlt-sys-color-principal-5: #1979E6;
  --mlt-sys-color-principal-6: #4B8EEC;
  --mlt-sys-color-principal-7: #80ADF4;
  --mlt-sys-color-principal-8: #B2CAFA;
  --mlt-sys-color-principal-9: #E6EDFE;



  --mlt-sys-color-alternativo-1: #190801;
  --mlt-sys-color-alternativo-2: #4D1D05;
  --mlt-sys-color-alternativo-3: #83370B;
  --mlt-sys-color-alternativo-4: #B45613;
  --mlt-sys-color-alternativo-5: #E67519;
  --mlt-sys-color-alternativo-6: #EC9C4B;
  --mlt-sys-color-alternativo-7: #F4BE80;
  --mlt-sys-color-alternativo-8: #FADCB2;
  --mlt-sys-color-alternativo-9: #FEF5E6;



  --mlt-sys-color-rojo-1: #190501;
  --mlt-sys-color-rojo-2: #4D0D05;
  --mlt-sys-color-rojo-3: #83150B;
  --mlt-sys-color-rojo-4: #B41813;
  --mlt-sys-color-rojo-5: #E61919;
  --mlt-sys-color-rojo-6: #EC534B;
  --mlt-sys-color-rojo-7: #F48A80;
  --mlt-sys-color-rojo-8: #FABCB2;
  --mlt-sys-color-rojo-9: #FEEAE6;



  --mlt-sys-color-gris-1: #1D2830;
  --mlt-sys-color-gris-2: #323E49;
  --mlt-sys-color-gris-3: #485461;
  --mlt-sys-color-gris-4: #5C6775;
  --mlt-sys-color-gris-5: #717D8E;
  --mlt-sys-color-gris-6: #8A93A3;
  --mlt-sys-color-gris-7: #A1A9BA;
  --mlt-sys-color-gris-8: #B6BDCD;
  --mlt-sys-color-gris-9: #CFD4E2;



  --mlt-sys-color-blanco-1: #EBEBEB;
  --mlt-sys-color-blanco-2: #EDEDED;
  --mlt-sys-color-blanco-3: #F0F0F0;
  --mlt-sys-color-blanco-4: #F2F2F2;
  --mlt-sys-color-blanco-5: #F5F5F5;
  --mlt-sys-color-blanco-6: #F7F7F7;
  --mlt-sys-color-blanco-7: #FAFAFA;
  --mlt-sys-color-blanco-8: #FCFCFC;
  --mlt-sys-color-blanco-9: #FFFFFF;



  --mlt-sys-color-amarillo-1: #6D5303;
  --mlt-sys-color-amarillo-2: #9A7809;
  --mlt-sys-color-amarillo-3: #C69C10;
  --mlt-sys-color-amarillo-4: #E9BC1C;
  --mlt-sys-color-amarillo-5: #ECC94B;
  --mlt-sys-color-amarillo-6: #F2D978;
  --mlt-sys-color-amarillo-7: #F7E7A6;
  --mlt-sys-color-amarillo-8: #FCF4CF;
  --mlt-sys-color-amarillo-9: #FFFFFF;



  --mlt-sys-font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mlt-sys-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --mlt-sys-font-serif: Palatino, "Palatino Linotype", "Book Antiqua", Georgia, "Times New Roman", Times, serif;
  --mlt-sys-font-default: var(--mlt-sys-font-sans-serif);

  --mlt-sys-font-size-1: 10px;
  --mlt-sys-font-size-2: 12px;
  --mlt-sys-font-size-3: 14px;
  --mlt-sys-font-size-4: 16px;
  --mlt-sys-font-size-5: 18px;
  --mlt-sys-font-size-6: 20px;
  --mlt-sys-font-size-7: 22px;
  --mlt-sys-font-size-8: 24px;
  --mlt-sys-font-size-9: 26px;
  --mlt-sys-font-size-10: 28px;
  --mlt-sys-font-size-11: 30px;

  --mlt-sys-margin-0: 0px;
  --mlt-sys-margin-1: 4px;
  --mlt-sys-margin-2: 8px;
  --mlt-sys-margin-3: 12px;
  --mlt-sys-margin-4: 16px;
  --mlt-sys-margin-5: 20px;
  --mlt-sys-margin-6: 24px;
  --mlt-sys-margin-7: 28px;
  --mlt-sys-margin-8: 32px;
  --mlt-sys-margin-9: 36px;
  --mlt-sys-margin-10: 40px;

  --mlt-sys-padding-0: 0px;
  --mlt-sys-padding-1: 4px;
  --mlt-sys-padding-2: 8px;
  --mlt-sys-padding-3: 12px;
  --mlt-sys-padding-4: 16px;
  --mlt-sys-padding-5: 20px;
  --mlt-sys-padding-6: 24px;
  --mlt-sys-padding-7: 28px;
  --mlt-sys-padding-8: 32px;
  --mlt-sys-padding-9: 36px;
  --mlt-sys-padding-10: 40px;

  --mlt-sys-border-radius-1: 1px;
  --mlt-sys-border-radius-2: 2px;
  --mlt-sys-border-radius-3: 4px;
  --mlt-sys-border-radius-4: 6px;

  --mlt-sys-width-0: 0px;
  --mlt-sys-width-1: 1px;
  --mlt-sys-width-2: 4px;
  --mlt-sys-width-3: 8px;
  --mlt-sys-width-4: 12px;
  --mlt-sys-width-5: 16px;
  --mlt-sys-width-6: 20px;
  --mlt-sys-width-7: 24px;
  --mlt-sys-width-8: 28px;
  --mlt-sys-width-9: 32px;
  --mlt-sys-width-10: 36px;
  --mlt-sys-width-11: 40px;

  --mlt-sys-oscurecimiento-0: 0%;
  --mlt-sys-oscurecimiento-1: 5%;
  --mlt-sys-oscurecimiento-2: 10%;
  --mlt-sys-oscurecimiento-3: 15%;
  --mlt-sys-oscurecimiento-4: 20%;
  --mlt-sys-oscurecimiento-5: 25%;
  --mlt-sys-oscurecimiento-6: 30%;
}

Esas variables se usarán siempre en vez de poner directamente números. El botón, ya con el nombre BEM del bloque (c-boton), queda así:

.c-boton {
  font-family: var(--mlt-sys-font-default);
  font-size: var(--mlt-sys-font-size-4);
  padding: var(--mlt-sys-padding-3);
 
  border-radius: var(--mlt-sys-border-radius-4);
  border-width: var(--mlt-sys-width-1);
  border-style: solid;
 
  display: inline-block;
  cursor: pointer;
  text-decoration: none;
 
 
  border-color: var(--mlt-sys-color-principal-5);
  background-color: var(--mlt-sys-color-principal-5);
  color: var(--mlt-sys-color-blanco-9);
}

Algunas de las variables de colores en la página principal de google, ¿te suena de algo?:

Tokens de diseño

Pero aun se tiene que mejorar la legibilidad del código. En vez de usar directamente los tokens de sistema, el botón crea sus propias variables:

.c-boton {
  /* Tokens de diseño: decisiones del diseñador, un valor cada uno */
  --boton-color-normal: var(--mlt-sys-color-principal-5);
  --boton-color-alternativa: var(--mlt-sys-color-alternativo-5);
  --boton-color-peligrosa: var(--mlt-sys-color-rojo-5);
  --boton-color-texto-primaria: var(--mlt-sys-color-blanco-9);

  --boton-font-family: var(--mlt-sys-font-default);
  --boton-font-size: var(--mlt-sys-font-size-4);
  --boton-padding: var(--mlt-sys-padding-3);
  --boton-border-width: var(--mlt-sys-width-1);
  --boton-border-radius: var(--mlt-sys-border-radius-4);


  /* Estado privado: por defecto, normal y primaria */
  --boton-privateState-color: var(--boton-color-normal);
  --boton-privateState-background-color: var(--boton-privateState-color);
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);

  /* Propiedades CSS: solo usan el estado y los tokens */
  font-family: var(--boton-font-family);
  font-size: var(--boton-font-size);
  padding: var(--boton-padding);

  border-radius: var(--boton-border-radius);
  border-width: var(--boton-border-width);
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;

  border-color: var(--boton-privateState-border-color);
  background-color: var(--boton-privateState-background-color);
  color: var(--boton-privateState-text-color);
}

/* La función elige qué token de color se usa */
.c-boton--funcion-normal {
  --boton-privateState-color: var(--boton-color-normal);
}
.c-boton--funcion-alternativa {
  --boton-privateState-color: var(--boton-color-alternativa);
}
.c-boton--funcion-peligrosa {
  --boton-privateState-color: var(--boton-color-peligrosa);
}

/* La importancia decide dónde va el color: fondo, borde o texto */
.c-boton--importancia-primaria {
  --boton-privateState-background-color: var(--boton-privateState-color);
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
}
.c-boton--importancia-secundaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-privateState-color);
}
.c-boton--importancia-terciaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: transparent;
  --boton-privateState-text-color: var(--boton-privateState-color);
}

<button class="c-boton c-boton--funcion-peligrosa c-boton--importancia-secundaria">Borrar</button>

Dentro del botón hay dos tipos de variables:

  • Tokens de diseño (--boton-*): Son las decisiones del diseñador: de qué color es el botón normal, el alternativo y el peligroso, el color del texto en la primaria, la fuente, el tamaño, el redondeo… Cada token tiene un único valor, que por defecto es un token de sistema.
  • Estado privado (--boton-privateState-*): Guarda cómo está el botón ahora mismo: qué color le toca y qué color va en el fondo, el borde y el texto. No son tokens sino algo informático: el CSS que hace falta para implementar el botón y que al diseñador no le interesa.

Para saber si una variable es un token o es estado: si su valor depende del modificador que lleve el botón, es estado. --boton-color-peligrosa vale siempre rojo; --boton-privateState-color vale el color que le toque según su función.

Por eso los modificadores solo cambian el estado, nunca los tokens:

  • Función: Elige qué token de color se usa. c-boton--funcion-peligrosa pone en --boton-privateState-color el token --boton-color-peligrosa.
  • Importancia: Decide dónde va ese color: en la primaria el fondo y el borde, en la secundaria el borde y el texto, y en la terciaria solo el texto. La secundaria y la terciaria tienen el fondo transparent, así que quedan bien sobre cualquier fondo. En la terciaria el borde también es transparent: está para que el botón no cambie de tamaño, pero no se ve. Eso es un truco de CSS, no una decisión de diseño.

transparent no es un token: es una palabra clave de CSS y no hay nada que el diseñador pueda elegir.

Token de diseño Estado privado
Qué guarda Una decisión del diseñador Cómo está el botón ahora
Nombre --boton-* --boton-privateState-*
Su valor Uno solo, por defecto un token de sistema Depende de los modificadores
Quién lo cambia El diseñador o quien personaliza el botón Solo los modificadores del botón
Ejemplo --boton-color-peligrosa --boton-privateState-background-color
Para que los botones peligrosos sean de otro rojo se cambia el token --boton-color-peligrosa, nunca el estado. Si en un botón secundario redefines --boton-privateState-background-color, pintas de rojo el fondo que debía ser transparente y el borde y el texto se quedan como estaban.
Los nombres de las variables CSS distinguen mayúsculas de minúsculas: --boton-privatestate-text-color es otra variable y el botón no funcionaría.

Añadiendo el tamaño

Ahora necesitamos botones de tres tamaños, con los modificadores c-boton--size-s, c-boton--size-m y c-boton--size-l. El tamaño cambia el font-size y el padding, así que el botón crece: más tokens de diseño, más estado privado y un modificador nuevo. Lo nuevo está resaltado:

.c-boton {
  /* Tokens de diseño: decisiones del diseñador, un valor cada uno */
  --boton-color-normal: var(--mlt-sys-color-principal-5);
  --boton-color-alternativa: var(--mlt-sys-color-alternativo-5);
  --boton-color-peligrosa: var(--mlt-sys-color-rojo-5);
  --boton-color-texto-primaria: var(--mlt-sys-color-blanco-9);

  --boton-font-family: var(--mlt-sys-font-default);
  --boton-font-size-s: var(--mlt-sys-font-size-3);
  --boton-font-size-m: var(--mlt-sys-font-size-4);
  --boton-font-size-l: var(--mlt-sys-font-size-6);
  --boton-padding-s: var(--mlt-sys-padding-2);
  --boton-padding-m: var(--mlt-sys-padding-3);
  --boton-padding-l: var(--mlt-sys-padding-4);
  --boton-border-width: var(--mlt-sys-width-1);
  --boton-border-radius: var(--mlt-sys-border-radius-4);


  /* Estado privado: por defecto, normal, primaria y tamaño m */
  --boton-privateState-color: var(--boton-color-normal);
  --boton-privateState-background-color: var(--boton-privateState-color);
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding: var(--boton-padding-m);

  /* Propiedades CSS: solo usan el estado y los tokens */
  font-family: var(--boton-font-family);
  font-size: var(--boton-privateState-font-size);
  padding: var(--boton-privateState-padding);

  border-radius: var(--boton-border-radius);
  border-width: var(--boton-border-width);
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;

  border-color: var(--boton-privateState-border-color);
  background-color: var(--boton-privateState-background-color);
  color: var(--boton-privateState-text-color);
}

/* La función elige qué token de color se usa */
.c-boton--funcion-normal {
  --boton-privateState-color: var(--boton-color-normal);
}
.c-boton--funcion-alternativa {
  --boton-privateState-color: var(--boton-color-alternativa);
}
.c-boton--funcion-peligrosa {
  --boton-privateState-color: var(--boton-color-peligrosa);
}

/* La importancia decide dónde va el color: fondo, borde o texto */
.c-boton--importancia-primaria {
  --boton-privateState-background-color: var(--boton-privateState-color);
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
}
.c-boton--importancia-secundaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-privateState-color);
}
.c-boton--importancia-terciaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: transparent;
  --boton-privateState-text-color: var(--boton-privateState-color);
}

/* El tamaño elige qué tokens de font-size y padding se usan */
.c-boton--size-s {
  --boton-privateState-font-size: var(--boton-font-size-s);
  --boton-privateState-padding: var(--boton-padding-s);
}
.c-boton--size-m {
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding: var(--boton-padding-m);
}
.c-boton--size-l {
  --boton-privateState-font-size: var(--boton-font-size-l);
  --boton-privateState-padding: var(--boton-padding-l);
}

<button class="c-boton c-boton--funcion-peligrosa c-boton--importancia-secundaria c-boton--size-s">Borrar</button>
<button class="c-boton c-boton--size-m">Guardar</button>
<button class="c-boton c-boton--funcion-alternativa c-boton--size-l">Comprar</button>

Lo que ha cambiado:

  • Tokens de diseño: --boton-font-size y --boton-padding pasan a ser un token por tamaño (--boton-font-size-s, -m, -l y lo mismo con el padding). Un token tiene un único valor, así que si hay tres tamaños hay tres tokens.
  • Estado privado: --boton-privateState-font-size y --boton-privateState-padding guardan el tamaño que le toca al botón ahora. Por defecto es el m, así que un botón sin modificador de tamaño sigue igual que antes.
  • Propiedades CSS: font-size y padding ya no usan el token directamente sino el estado.
  • Modificador --size-*: Como los de función e importancia, solo cambia el estado: elige qué tokens de font-size y padding se usan.

El tamaño no toca las variables de la función ni de la importancia, por eso se combina con ellas sin problemas: un botón puede ser peligroso, secundario y pequeño a la vez. Cada nuevo modificador es un grupo más de estado privado.

Si en .c-boton--size-l pones directamente font-size: var(--boton-font-size-l), funciona, pero el botón ya no sigue la regla de que las propiedades CSS solo usan el estado y los tokens. Los modificadores siempre cambian el estado, nunca las propiedades CSS.

Padding vertical y horizontal

El último extra: el padding vertical y el horizontal no tienen por qué ser iguales. Un botón queda mejor con más padding a los lados que arriba y abajo, así que cada tamaño necesita dos paddings. Otra vez el botón crece: más tokens de diseño y más estado privado, pero ningún modificador nuevo. Lo nuevo está resaltado:

.c-boton {
  /* Tokens de diseño: decisiones del diseñador, un valor cada uno */
  --boton-color-normal: var(--mlt-sys-color-principal-5);
  --boton-color-alternativa: var(--mlt-sys-color-alternativo-5);
  --boton-color-peligrosa: var(--mlt-sys-color-rojo-5);
  --boton-color-texto-primaria: var(--mlt-sys-color-blanco-9);

  --boton-font-family: var(--mlt-sys-font-default);
  --boton-font-size-s: var(--mlt-sys-font-size-3);
  --boton-font-size-m: var(--mlt-sys-font-size-4);
  --boton-font-size-l: var(--mlt-sys-font-size-6);
  --boton-padding-vertical-s: var(--mlt-sys-padding-1);
  --boton-padding-vertical-m: var(--mlt-sys-padding-2);
  --boton-padding-vertical-l: var(--mlt-sys-padding-3);
  --boton-padding-horizontal-s: var(--mlt-sys-padding-3);
  --boton-padding-horizontal-m: var(--mlt-sys-padding-4);
  --boton-padding-horizontal-l: var(--mlt-sys-padding-6);
  --boton-border-width: var(--mlt-sys-width-1);
  --boton-border-radius: var(--mlt-sys-border-radius-4);


  /* Estado privado: por defecto, normal, primaria y tamaño m */
  --boton-privateState-color: var(--boton-color-normal);
  --boton-privateState-background-color: var(--boton-privateState-color);
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-m);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-m);

  /* Propiedades CSS: solo usan el estado y los tokens */
  font-family: var(--boton-font-family);
  font-size: var(--boton-privateState-font-size);
  padding: var(--boton-privateState-padding-vertical) var(--boton-privateState-padding-horizontal);

  border-radius: var(--boton-border-radius);
  border-width: var(--boton-border-width);
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;

  border-color: var(--boton-privateState-border-color);
  background-color: var(--boton-privateState-background-color);
  color: var(--boton-privateState-text-color);
}

/* La función elige qué token de color se usa */
.c-boton--funcion-normal {
  --boton-privateState-color: var(--boton-color-normal);
}
.c-boton--funcion-alternativa {
  --boton-privateState-color: var(--boton-color-alternativa);
}
.c-boton--funcion-peligrosa {
  --boton-privateState-color: var(--boton-color-peligrosa);
}

/* La importancia decide dónde va el color: fondo, borde o texto */
.c-boton--importancia-primaria {
  --boton-privateState-background-color: var(--boton-privateState-color);
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
}
.c-boton--importancia-secundaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: var(--boton-privateState-color);
  --boton-privateState-text-color: var(--boton-privateState-color);
}
.c-boton--importancia-terciaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: transparent;
  --boton-privateState-text-color: var(--boton-privateState-color);
}

/* El tamaño elige qué tokens de font-size y padding se usan */
.c-boton--size-s {
  --boton-privateState-font-size: var(--boton-font-size-s);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-s);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-s);
}
.c-boton--size-m {
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-m);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-m);
}
.c-boton--size-l {
  --boton-privateState-font-size: var(--boton-font-size-l);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-l);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-l);
}

Lo que ha cambiado:

  • Tokens de diseño: Cada --boton-padding-* se parte en dos, --boton-padding-vertical-* y --boton-padding-horizontal-*. Con tres tamaños son seis tokens.
  • Estado privado: --boton-privateState-padding se parte también en --boton-privateState-padding-vertical y --boton-privateState-padding-horizontal.
  • Propiedades CSS: padding recibe los dos valores: el primero es el vertical y el segundo el horizontal.
  • Modificadores --size-*: Siguen siendo los mismos, solo que ahora ponen dos variables de padding en vez de una.

El HTML no cambia: los botones se siguen usando igual que antes. Eso es lo bueno de separar tokens, estado y propiedades: el componente crece por dentro sin que se entere quien lo usa.

Oscurecer al pasar el ratón

Ahora, al pasar el ratón por encima, el color del botón se tiene que oscurecer un poco. Para oscurecer un color se mezcla con negro usando color-mix():

color-mix(in oklab, var(--boton-privateState-color), black 15%)

Esta vez no basta con añadir tokens de diseño y estado privado: también hacen falta tokens de sistema nuevos. Para el tamaño y el padding ya había valores en el sistema, pero no hay ningún oscurecimiento, y el 15% puesto a mano sería un número mágico. Así que se añade al sistema una escala de oscurecimientos, igual que la de paddings, y cada componente elige de ella el que quiera en sus tokens de diseño, para el hover o para lo que necesite:

:root {
  --mlt-sys-oscurecimiento-0: 0%;
  --mlt-sys-oscurecimiento-1: 5%;
  --mlt-sys-oscurecimiento-2: 10%;
  --mlt-sys-oscurecimiento-3: 15%;
  --mlt-sys-oscurecimiento-4: 20%;
  --mlt-sys-oscurecimiento-5: 25%;
  --mlt-sys-oscurecimiento-6: 30%;
}

En el botón hay que pensar qué es token y qué es estado:

  • Token de diseño --boton-hover-oscurecimiento: Cuánto se oscurece. Es una decisión del diseñador y vale siempre lo mismo. Como todos los tokens de diseño, su valor por defecto es un token de sistema: --mlt-sys-oscurecimiento-3 (15%).
  • Estado privado --boton-privateState-oscurecimiento: Cuánto está oscurecido el botón ahora mismo: 0% normalmente y el valor del token cuando el ratón está encima.
  • Estado privado --boton-privateState-color-final: El color que se pinta de verdad, que es el color de la función mezclado con negro según el oscurecimiento. El fondo, el borde y el texto pasan a usar este en vez de --boton-privateState-color.
  • :hover: Funciona como un modificador más que pone el navegador: solo cambia el estado.

Lo nuevo está resaltado:

.c-boton {
  /* Tokens de diseño: decisiones del diseñador, un valor cada uno */
  --boton-color-normal: var(--mlt-sys-color-principal-5);
  --boton-color-alternativa: var(--mlt-sys-color-alternativo-5);
  --boton-color-peligrosa: var(--mlt-sys-color-rojo-5);
  --boton-color-texto-primaria: var(--mlt-sys-color-blanco-9);

  --boton-font-family: var(--mlt-sys-font-default);
  --boton-font-size-s: var(--mlt-sys-font-size-3);
  --boton-font-size-m: var(--mlt-sys-font-size-4);
  --boton-font-size-l: var(--mlt-sys-font-size-6);
  --boton-padding-vertical-s: var(--mlt-sys-padding-1);
  --boton-padding-vertical-m: var(--mlt-sys-padding-2);
  --boton-padding-vertical-l: var(--mlt-sys-padding-3);
  --boton-padding-horizontal-s: var(--mlt-sys-padding-3);
  --boton-padding-horizontal-m: var(--mlt-sys-padding-4);
  --boton-padding-horizontal-l: var(--mlt-sys-padding-6);
  --boton-border-width: var(--mlt-sys-width-1);
  --boton-border-radius: var(--mlt-sys-border-radius-4);
  --boton-hover-oscurecimiento: var(--mlt-sys-oscurecimiento-3);


  /* Estado privado: por defecto, normal, primaria, tamaño m y sin hover */
  --boton-privateState-color: var(--boton-color-normal);
  --boton-privateState-oscurecimiento: 0%;
  --boton-privateState-color-final: color-mix(in oklab, var(--boton-privateState-color), black var(--boton-privateState-oscurecimiento));
  --boton-privateState-background-color: var(--boton-privateState-color-final);
  --boton-privateState-border-color: var(--boton-privateState-color-final);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-m);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-m);

  /* Propiedades CSS: solo usan el estado y los tokens */
  font-family: var(--boton-font-family);
  font-size: var(--boton-privateState-font-size);
  padding: var(--boton-privateState-padding-vertical) var(--boton-privateState-padding-horizontal);

  border-radius: var(--boton-border-radius);
  border-width: var(--boton-border-width);
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;

  border-color: var(--boton-privateState-border-color);
  background-color: var(--boton-privateState-background-color);
  color: var(--boton-privateState-text-color);
}

/* La función elige qué token de color se usa */
.c-boton--funcion-normal {
  --boton-privateState-color: var(--boton-color-normal);
}
.c-boton--funcion-alternativa {
  --boton-privateState-color: var(--boton-color-alternativa);
}
.c-boton--funcion-peligrosa {
  --boton-privateState-color: var(--boton-color-peligrosa);
}

/* La importancia decide dónde va el color: fondo, borde o texto */
.c-boton--importancia-primaria {
  --boton-privateState-background-color: var(--boton-privateState-color-final);
  --boton-privateState-border-color: var(--boton-privateState-color-final);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
}
.c-boton--importancia-secundaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: var(--boton-privateState-color-final);
  --boton-privateState-text-color: var(--boton-privateState-color-final);
}
.c-boton--importancia-terciaria {
  --boton-privateState-background-color: transparent;
  --boton-privateState-border-color: transparent;
  --boton-privateState-text-color: var(--boton-privateState-color-final);
}

/* El tamaño elige qué tokens de font-size y padding se usan */
.c-boton--size-s {
  --boton-privateState-font-size: var(--boton-font-size-s);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-s);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-s);
}
.c-boton--size-m {
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-m);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-m);
}
.c-boton--size-l {
  --boton-privateState-font-size: var(--boton-font-size-l);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-l);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-l);
}

/* Al pasar el ratón, el color se oscurece */
.c-boton:hover {
  --boton-privateState-oscurecimiento: var(--boton-hover-oscurecimiento);
}

El texto blanco de la primaria no se oscurece porque no usa el color final sino el token --boton-color-texto-primaria. En la secundaria y la terciaria sí se oscurecen el texto y el borde, porque usan el color final.

Lo primero que se le ocurre a cualquiera es --boton-privateState-color: color-mix(in oklab, var(--boton-privateState-color), black 15%) dentro del :hover. No funciona: una variable CSS no puede usarse a sí misma en su valor (es un ciclo) y el navegador la anula, así que el botón se queda sin color. Por eso hace falta la variable nueva --boton-privateState-color-final.

Comparación con Angular Material

Nuestro sistema es una versión sencilla de lo que hace Angular Material:

Angular Material Nuestro sistema
Token de sistema --mat-sys-primary --mlt-sys-color-principal-5
Token de diseño --mat-button-filled-container-color --boton-color-normal
El token de diseño toma el valor de un token de sistema var(--mat-button-filled-container-color, var(--mat-sys-primary)) --boton-color-normal: var(--mlt-sys-color-principal-5)
Cambiar un componente mixin mat.button-overrides(…) redefinir sus tokens de diseño --boton-* (nunca el privateState)
Cambiar todo el sistema el tema (mat.theme) cambiar las variables de SASS que generan las --mlt-sys-*
Variantes del componente matButton="filled", "outlined", "text" modificadores --importancia-primaria, -secundaria, -terciaria

Mejorando los tokens con SASS

Tokens de sistema

Escribir a mano todos los tokens de sistema es largo y es fácil equivocarse. Con SASS se guardan los valores en listas y mapas y los tokens se generan con bucles. Así, para cambiar el sistema solo hay que cambiar las variables de SASS. Los colores se generan igual que en el ejercicio 1 de SASS, ahora con el prefijo --mlt-sys-color-. Estos son todos los tokens de sistema que usa el botón:

@use "sass:list";
@use "sass:meta";

$colores: (
  'principal': (#011019, #052F4D, #0B4D83, #1363B4, #1979E6, #4B8EEC, #80ADF4, #B2CAFA, #E6EDFE),
  'alternativo': (#190801, #4D1D05, #83370B, #B45613, #E67519, #EC9C4B, #F4BE80, #FADCB2, #FEF5E6),
  'rojo': (#190501, #4D0D05, #83150B, #B41813, #E61919, #EC534B, #F48A80, #FABCB2, #FEEAE6),
  'gris': (#1D2830, #323E49, #485461, #5C6775, #717D8E, #8A93A3, #A1A9BA, #B6BDCD, #CFD4E2),
  'blanco': (#EBEBEB, #EDEDED, #F0F0F0, #F2F2F2, #F5F5F5, #F7F7F7, #FAFAFA, #FCFCFC, #FFFFFF),
  'amarillo': (#6D5303, #9A7809, #C69C10, #E9BC1C, #ECC94B, #F2D978, #F7E7A6, #FCF4CF, #FFFFFF)
);

$fuentes: (
  'sans-serif': (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif),
  'monospace': (SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace),
  'serif': (Palatino, "Palatino Linotype", "Book Antiqua", Georgia, "Times New Roman", Times, serif)
);

$font-sizes: (10px, 12px, 14px, 16px, 18px, 20px, 22px, 24px, 26px, 28px, 30px);
$paddings: (0px, 4px, 8px, 12px, 16px, 20px, 24px, 28px, 32px, 36px, 40px);
$border-radius: (1px, 2px, 4px, 6px);
$widths: (0px, 1px, 4px, 8px, 12px, 16px, 20px, 24px, 28px, 32px, 36px, 40px);
$oscurecimientos: (0%, 5%, 10%, 15%, 20%, 25%, 30%);

:root {
  @each $nombre, $tonos in $colores {
    @for $i from 1 through list.length($tonos) {
      --mlt-sys-color-#{$nombre}-#{$i}: #{list.nth($tonos, $i)};
    }
  }

  @each $nombre, $fuente in $fuentes {
    --mlt-sys-font-#{$nombre}: #{meta.inspect($fuente)};
  }
  --mlt-sys-font-default: var(--mlt-sys-font-sans-serif);

  @for $i from 1 through list.length($font-sizes) {
    --mlt-sys-font-size-#{$i}: #{list.nth($font-sizes, $i)};
  }

  // Los paddings, los anchos y los oscurecimientos empiezan en 0
  @for $i from 0 through list.length($paddings) - 1 {
    --mlt-sys-padding-#{$i}: #{list.nth($paddings, $i + 1)};
  }

  @for $i from 1 through list.length($border-radius) {
    --mlt-sys-border-radius-#{$i}: #{list.nth($border-radius, $i)};
  }

  @for $i from 0 through list.length($widths) - 1 {
    --mlt-sys-width-#{$i}: #{list.nth($widths, $i + 1)};
  }

  @for $i from 0 through list.length($oscurecimientos) - 1 {
    --mlt-sys-oscurecimiento-#{$i}: #{list.nth($oscurecimientos, $i + 1)};
  }
}

se transforma en

:root {
  --mlt-sys-color-principal-1: #011019;
  --mlt-sys-color-principal-2: #052F4D;
  --mlt-sys-color-principal-3: #0B4D83;
  --mlt-sys-color-principal-4: #1363B4;
  --mlt-sys-color-principal-5: #1979E6;
  --mlt-sys-color-principal-6: #4B8EEC;
  --mlt-sys-color-principal-7: #80ADF4;
  --mlt-sys-color-principal-8: #B2CAFA;
  --mlt-sys-color-principal-9: #E6EDFE;
  --mlt-sys-color-alternativo-1: #190801;
  --mlt-sys-color-alternativo-2: #4D1D05;
  --mlt-sys-color-alternativo-3: #83370B;
  --mlt-sys-color-alternativo-4: #B45613;
  --mlt-sys-color-alternativo-5: #E67519;
  --mlt-sys-color-alternativo-6: #EC9C4B;
  --mlt-sys-color-alternativo-7: #F4BE80;
  --mlt-sys-color-alternativo-8: #FADCB2;
  --mlt-sys-color-alternativo-9: #FEF5E6;
  --mlt-sys-color-rojo-1: #190501;
  --mlt-sys-color-rojo-2: #4D0D05;
  --mlt-sys-color-rojo-3: #83150B;
  --mlt-sys-color-rojo-4: #B41813;
  --mlt-sys-color-rojo-5: #E61919;
  --mlt-sys-color-rojo-6: #EC534B;
  --mlt-sys-color-rojo-7: #F48A80;
  --mlt-sys-color-rojo-8: #FABCB2;
  --mlt-sys-color-rojo-9: #FEEAE6;
  --mlt-sys-color-gris-1: #1D2830;
  --mlt-sys-color-gris-2: #323E49;
  --mlt-sys-color-gris-3: #485461;
  --mlt-sys-color-gris-4: #5C6775;
  --mlt-sys-color-gris-5: #717D8E;
  --mlt-sys-color-gris-6: #8A93A3;
  --mlt-sys-color-gris-7: #A1A9BA;
  --mlt-sys-color-gris-8: #B6BDCD;
  --mlt-sys-color-gris-9: #CFD4E2;
  --mlt-sys-color-blanco-1: #EBEBEB;
  --mlt-sys-color-blanco-2: #EDEDED;
  --mlt-sys-color-blanco-3: #F0F0F0;
  --mlt-sys-color-blanco-4: #F2F2F2;
  --mlt-sys-color-blanco-5: #F5F5F5;
  --mlt-sys-color-blanco-6: #F7F7F7;
  --mlt-sys-color-blanco-7: #FAFAFA;
  --mlt-sys-color-blanco-8: #FCFCFC;
  --mlt-sys-color-blanco-9: #FFFFFF;
  --mlt-sys-color-amarillo-1: #6D5303;
  --mlt-sys-color-amarillo-2: #9A7809;
  --mlt-sys-color-amarillo-3: #C69C10;
  --mlt-sys-color-amarillo-4: #E9BC1C;
  --mlt-sys-color-amarillo-5: #ECC94B;
  --mlt-sys-color-amarillo-6: #F2D978;
  --mlt-sys-color-amarillo-7: #F7E7A6;
  --mlt-sys-color-amarillo-8: #FCF4CF;
  --mlt-sys-color-amarillo-9: #FFFFFF;
  --mlt-sys-font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mlt-sys-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --mlt-sys-font-serif: Palatino, "Palatino Linotype", "Book Antiqua", Georgia, "Times New Roman", Times, serif;
  --mlt-sys-font-default: var(--mlt-sys-font-sans-serif);
  --mlt-sys-font-size-1: 10px;
  --mlt-sys-font-size-2: 12px;
  --mlt-sys-font-size-3: 14px;
  --mlt-sys-font-size-4: 16px;
  --mlt-sys-font-size-5: 18px;
  --mlt-sys-font-size-6: 20px;
  --mlt-sys-font-size-7: 22px;
  --mlt-sys-font-size-8: 24px;
  --mlt-sys-font-size-9: 26px;
  --mlt-sys-font-size-10: 28px;
  --mlt-sys-font-size-11: 30px;
  --mlt-sys-padding-0: 0px;
  --mlt-sys-padding-1: 4px;
  --mlt-sys-padding-2: 8px;
  --mlt-sys-padding-3: 12px;
  --mlt-sys-padding-4: 16px;
  --mlt-sys-padding-5: 20px;
  --mlt-sys-padding-6: 24px;
  --mlt-sys-padding-7: 28px;
  --mlt-sys-padding-8: 32px;
  --mlt-sys-padding-9: 36px;
  --mlt-sys-padding-10: 40px;
  --mlt-sys-border-radius-1: 1px;
  --mlt-sys-border-radius-2: 2px;
  --mlt-sys-border-radius-3: 4px;
  --mlt-sys-border-radius-4: 6px;
  --mlt-sys-width-0: 0px;
  --mlt-sys-width-1: 1px;
  --mlt-sys-width-2: 4px;
  --mlt-sys-width-3: 8px;
  --mlt-sys-width-4: 12px;
  --mlt-sys-width-5: 16px;
  --mlt-sys-width-6: 20px;
  --mlt-sys-width-7: 24px;
  --mlt-sys-width-8: 28px;
  --mlt-sys-width-9: 32px;
  --mlt-sys-width-10: 36px;
  --mlt-sys-width-11: 40px;
  --mlt-sys-oscurecimiento-0: 0%;
  --mlt-sys-oscurecimiento-1: 5%;
  --mlt-sys-oscurecimiento-2: 10%;
  --mlt-sys-oscurecimiento-3: 15%;
  --mlt-sys-oscurecimiento-4: 20%;
  --mlt-sys-oscurecimiento-5: 25%;
  --mlt-sys-oscurecimiento-6: 30%;
}

Los márgenes, que no usa el botón, se generan igual que los paddings.

Tokens de diseño

El botón con sus modificadores, con el selector padre &:

.c-boton {
  // Tokens de diseño: decisiones del diseñador, un valor cada uno
  --boton-color-normal: var(--mlt-sys-color-principal-5);
  --boton-color-alternativa: var(--mlt-sys-color-alternativo-5);
  --boton-color-peligrosa: var(--mlt-sys-color-rojo-5);
  --boton-color-texto-primaria: var(--mlt-sys-color-blanco-9);

  --boton-font-family: var(--mlt-sys-font-default);
  --boton-font-size-s: var(--mlt-sys-font-size-3);
  --boton-font-size-m: var(--mlt-sys-font-size-4);
  --boton-font-size-l: var(--mlt-sys-font-size-6);
  --boton-padding-vertical-s: var(--mlt-sys-padding-1);
  --boton-padding-vertical-m: var(--mlt-sys-padding-2);
  --boton-padding-vertical-l: var(--mlt-sys-padding-3);
  --boton-padding-horizontal-s: var(--mlt-sys-padding-3);
  --boton-padding-horizontal-m: var(--mlt-sys-padding-4);
  --boton-padding-horizontal-l: var(--mlt-sys-padding-6);
  --boton-border-width: var(--mlt-sys-width-1);
  --boton-border-radius: var(--mlt-sys-border-radius-4);
  --boton-hover-oscurecimiento: var(--mlt-sys-oscurecimiento-3);


  // Estado privado: por defecto, normal, primaria, tamaño m y sin hover
  --boton-privateState-color: var(--boton-color-normal);
  --boton-privateState-oscurecimiento: 0%;
  --boton-privateState-color-final: color-mix(in oklab, var(--boton-privateState-color), black var(--boton-privateState-oscurecimiento));
  --boton-privateState-background-color: var(--boton-privateState-color-final);
  --boton-privateState-border-color: var(--boton-privateState-color-final);
  --boton-privateState-text-color: var(--boton-color-texto-primaria);
  --boton-privateState-font-size: var(--boton-font-size-m);
  --boton-privateState-padding-vertical: var(--boton-padding-vertical-m);
  --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-m);

  // Propiedades CSS: solo usan el estado y los tokens
  font-family: var(--boton-font-family);
  font-size: var(--boton-privateState-font-size);
  padding: var(--boton-privateState-padding-vertical) var(--boton-privateState-padding-horizontal);

  border-radius: var(--boton-border-radius);
  border-width: var(--boton-border-width);
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;

  border-color: var(--boton-privateState-border-color);
  background-color: var(--boton-privateState-background-color);
  color: var(--boton-privateState-text-color);

  // La función elige qué token de color se usa
  &--funcion-normal {
    --boton-privateState-color: var(--boton-color-normal);
  }
  &--funcion-alternativa {
    --boton-privateState-color: var(--boton-color-alternativa);
  }
  &--funcion-peligrosa {
    --boton-privateState-color: var(--boton-color-peligrosa);
  }

  // La importancia decide dónde va el color: fondo, borde o texto
  &--importancia-primaria {
    --boton-privateState-background-color: var(--boton-privateState-color-final);
    --boton-privateState-border-color: var(--boton-privateState-color-final);
    --boton-privateState-text-color: var(--boton-color-texto-primaria);
  }
  &--importancia-secundaria {
    --boton-privateState-background-color: transparent;
    --boton-privateState-border-color: var(--boton-privateState-color-final);
    --boton-privateState-text-color: var(--boton-privateState-color-final);
  }
  &--importancia-terciaria {
    --boton-privateState-background-color: transparent;
    --boton-privateState-border-color: transparent;
    --boton-privateState-text-color: var(--boton-privateState-color-final);
  }

  // El tamaño elige qué tokens de font-size y padding se usan
  &--size-s {
    --boton-privateState-font-size: var(--boton-font-size-s);
    --boton-privateState-padding-vertical: var(--boton-padding-vertical-s);
    --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-s);
  }
  &--size-m {
    --boton-privateState-font-size: var(--boton-font-size-m);
    --boton-privateState-padding-vertical: var(--boton-padding-vertical-m);
    --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-m);
  }
  &--size-l {
    --boton-privateState-font-size: var(--boton-font-size-l);
    --boton-privateState-padding-vertical: var(--boton-padding-vertical-l);
    --boton-privateState-padding-horizontal: var(--boton-padding-horizontal-l);
  }

  // Al pasar el ratón, el color se oscurece
  &:hover {
    --boton-privateState-oscurecimiento: var(--boton-hover-oscurecimiento);
  }
}

Ejercicios

Ejercicio 1

Los paddings y los márgenes van de 4 en 4, así que no hace falta escribirlos en una lista: se pueden calcular. Dadas las variables:

$paso: 4px;
$numero-pasos: 10;

Genera con un @for los tokens de sistema de paddings y márgenes sin escribir ninguna lista. Empiezan en 0:

:root {
  --mlt-sys-padding-0: 0px;
  --mlt-sys-padding-1: 4px;
  ...
  --mlt-sys-padding-10: 40px;
  --mlt-sys-margin-0: 0px;
  ...
  --mlt-sys-margin-10: 40px;
}

Cambia $paso a 5px y comprueba que cambian todos los paddings y márgenes, y con ellos el botón.

Ejercicio 2

Al pulsar el botón (:active) se tiene que oscurecer más que con el ratón encima: un 25%.

  • Di qué tokens de sistema, tokens de diseño y estados privados hacen falta, y cuáles de ellos son nuevos.
  • Añádelo al botón en SASS sin tocar sus propiedades CSS ni los modificadores que ya tiene.

Comprueba en el navegador que el botón se oscurece un poco al pasar el ratón y más al pulsarlo, en las tres importancias.

Ejercicio 3

Junta en un único fichero SASS todas las variables de SASS del sistema (colores, tamaños de fuente, márgenes, paddings, oscurecimientos, fuentes, border-radius y anchos) y genera con él todo el :root de los tokens de sistema del tema.

Cambia ahora el color principal por un verde (genera sus 9 tonos con el Color Scheme Generator) y comprueba que el botón cambia de color sin tocar su CSS.

Ejercicio 4

Haz en SASS el componente c-panel con tokens de diseño y estado privado, igual que el botón, para que se puedan hacer estos 3 paneles:

  • Los tokens de diseño se llaman --panel-* y su valor por defecto es un token de sistema.
  • El color lo elige la función, con los modificadores c-panel--funcion-normal, c-panel--funcion-alternativa y c-panel--funcion-peligrosa.
  • Los elementos c-panel__titulo y c-panel__contenido no llevan modificadores: el color les llega por el estado privado del bloque.

Crea una página HTML con los 3 paneles y comprueba que, al cambiar el color principal por el verde del ejercicio 3, el panel normal también cambia sin tocar su CSS.

clase/daw/diw/sistema-diseno-tokens.txt · Última modificación: por claude