En Mi Local Funciona

Technical thoughts, stories and ideas

TailwindCSS, escribir CSS nunca fue tan fácil

Publicado por Jesús Mur el

Tailwind CSSFront

Seamos sinceros, escribir CSS la mayoría de las veces no es una tarea agradable; es una tarea que requiere de un trabajo artesanal y que suele llevar tiempo.

Tenemos librerías como Bootstrap CSS o Foundation que nos ayudan bastante a aplicar estilos bonitos a nuestras aplicaciones pero el problema es que al final, aunque estos frameworks se pueden personalizar, nos quedan aplicaciones muy parecidas unas de otras.

Por eso quiero hablaros de TailwindCSS.
alt

El creador de TailwindCSS, Adam Wathan, la define como una librería de utilidades agnóstica a los frameworks frontend, es decir, puede funcionar con cualquier herramienta ya sea Angular, React, Vue... Además, Tailwind no es un framework CSS como los mencionados anteriormente que están basados en componentes, Tailwind lo que nos provee son clases CSS predefinidas a más bajo nivel por lo que la creación de los componentes será cosa nuestra.

Instalación

Vamos a ver como funciona junto a Angular mientras desarrollamos una aplicación que vamos a bautizar con el nombre de AtGram.

alt

Primero vamos a realizar la instalación de Tailwind. Para ello escribimos en el terminal el comando:

npm install tailwindcss

Aparte, vamos a hacer algunos cambios en el archivo package.json:

...
 "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "tw": "node_modules\\.bin\\tailwind build ./src/tailwind.css -c ./tailwind-config.js -o ./src/styles.css",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "tailwind": "./node_modules/.bin/tailwind init"
  },
...

Disclaimer: En Windows debemos escribir las \\ para las rutas. En sistemas Unix usaremos /.

Acto seguido necesitamos crear el archivo de configuración de Tailwind con el siguiente comando:

npx tailwind init

Si escribimos npx tailwind init --full nos mostrará en el archivo tailwind-config.js las configuraciones predefinidas de Tailwind, pero no es necesario. De hecho es mucho mejor no tenerlas, pues así vemos claramente las modificaciones que nosotros le hemos implementado a Tailwind.

A continuación, escribimos los imports de Tailwind en el archivo src/tailwind.css y ejecutamos el comando npm run tw

tailwind.css
@tailwind base;

@tailwind components;

@tailwind utilities;

Ahora solo nos queda levantar el proyecto con npm start y ya podemos utilizar TailwindCSS.

Comencemos con el desarrollo de AtGram

Para comprobar que todo funciona correctamente vamos a borrar el contenido de nuestro app.component.html y vamos a dejarlo de la siguiente forma:

app.component.html
<div class="bg-orange-400">  
 <h1>Hello AtGram, this is TailwindCSS!</h1>
</div>

<router-outlet></router-outlet>  

Ahora como podréis comprobar veremos en pantalla la frase Hello AtGram, this is TailwindCSS! con un fondo naranja como el color corporativo de atSistemas que es el principal partner de este blog.

Una vez que vemos que todo funciona correctamente procederemos a programar la UI de nuestro AtGram.

Vamos a cambiar el contenido de nuestro componente raíz app.component.html por el siguiente código

app.component.html
<div class="bg-gray-100 h-screen">

</div>

<router-outlet></router-outlet>  

Con la clase bg-gray-100 le estamos diciendo a nuestra aplicación que el fondo (background) va a ser de color gris con la escala 100 y con la clase h-screen conseguiremos que el alto de la web sea el 100% del viewport.

Ahora vamos a crear nuestro primer componente que formará el header de la página. Para eso escribimos en nuestro terminal ng g c header.

A continuación, pasamos a construir el header dentro del archivo header/header.component.html, por lo que crearemos la siguiente estructura:

header.component.html
<header  
  (window:scroll)="doSomethingOnWindowsScroll($event)"
  class="sticky top-0 bg-white"
>
  <div #container class="md:container xl:container m-auto flex h-20">
    <div
      class="xs:w-1/2 sm:w-1/2 md:w-1/2 xl:w-1/3 flex xs:justify-start xs:mx-4 sm:mx-4 sm:justify-start md:justify-center xl:justify-center items-center"
    >
      <div
        id="atgram-logo"
        class="bg-no-repeat items-center"
        style="background-image: url('./assets/images/instagram_icons.png'); background-position: -74px -231px; height: 24px; width: 24px;"
      ></div>
      <div
        #slash
        id="slash"
        class="bg-black mt-0 mx-4 h-12"
        style="width: 1px;"
      ></div>
      <div
        #atgramLogo
        id="atgram-logo-text"
        class="bg-no-repeat mt-2 items-center"
        style="background-image: url('./assets/images/instagram_icons.png'); background-position: -74px -200px; height: 29px; width: 103px;"
      ></div>
    </div>

    <div
      class="w-1/3 xs:hidden sm:hidden md:hidden xl:flex justify-center items-center px-24"
    >
      <input
        class="search-input-width border rounded bg-gray-100"
        type="text"
        name="search"
        id="search"
        placeholder="Busca"
        autocomplete="off"
      />
    </div>

    <div class="xs:w-1/2 sm:w-1/2  md:w-1/2 xl:w-1/3">
      <div
        class="flex xs:justify-end sm:justify-end md:justify-center xl:justify-center items-center mt-1 h-20"
      >
        <div
          class="bg-no-repeat mx-4"
          id="atgram-explore"
          style="background-image: url('./assets/images/instagram_icons.png'); background-position: -257px -200px; height: 24px; width: 24px;"
        ></div>
        <div
          class="bg-no-repeat mx-4"
          id="atgram-heart"
          style="background-image: url('./assets/images/instagram_icons.png'); background-position: -26px -300px; height: 24px; width: 24px;"
        ></div>
        <div
          class="bg-no-repeat mx-4"
          id="atgram-profile"
          style="background-image: url('./assets/images/instagram_icons.png'); background-position: -234px -274px; height: 24px; width: 24px;"
        ></div>
      </div>
    </div>
  </div>
</header>  

Después incluiremos nuestro componente en el app.component.html con lo cual quedará de la siguiente forma:

alt

app.component.html
<div class="bg-gray-100 h-screen">  
  <app-header></app-header>
</div>

<router-outlet></router-outlet>  

De momento vamos a obviar el evento de scroll y vamos a pasar a explicar un poco las clases CSS de Tailwind que es de lo que trata este artículo. Como verás, en primer lugar tenemos una clase container que va a limitar el ancho de nuestra página en 768 pixeles en el caso de dispositivos medios y en 1280 pixeles en el caso de dispositivos grandes como por ejemplo monitores o televisores.

El container no centra el contenido horizontalmente, por lo que podemos añadir la clase mx-auto o bien ir al archivo de configuración de Tailwind tailwind-config.js y decirle que a partir de ahora nuestros containers se centraran horizontalmente.

tailwind-config.js
module.exports = {  
  theme: {
    container: {
      center: true,
    },
  },
}

Grid System

Para crear el grid de nuestra aplicación usaremos la característica de Tailwind que nos permite utilizar anchos basados por porcentaje. En el ejemplo anterior, le estamos indicando que en tamaños pequeños y medianos de pantalla tengamos 2 columnas de un 50% de ancho cada una w-1/2. En pantallas grandes pasaremos a tener 3 columnas con tamaño de w-1/3 cada una haciendo que la columna central desaparezca xs:hidden sm:hidden md:hidden.

Responsive Design

Como has podido ver en el apartado anterior, para definir los cambios que deben producirse en cada tipo de pantalla añadimos el prefijo sm: para pantallas pequeñas, md: para pantallas medianas, lg: para pantallas grandes y xl: para pantallas extra grandes.

/* sm */
@media (min-width: 640px) { /* ... */ }
/* md */
@media (min-width: 768px) { /* ... */ }
/* lg */
@media (min-width: 1024px) { /* ... */ }
/* xl */
@media (min-width: 1280px) { /* ... */ }

Para nuestro ejemplo vamos a crear un nuevo tamaño de pantalla para teléfonos móviles pequeños de menos de 400px de ancho, para eso vamos a necesitar incluir un nuevo breakpoint en el archivo de configuración que llamaremos xs: tailwind-config.js

tailwind-config.js
module.exports = {  
  prefix: '',
  important: false,
  separator: ':',
  theme: {
    screens: {
      xs: '400px',
      sm: '640px',
      md: '768px',
      lg: '1024px',
      xl: '1280px'
    },
   ...
   }
 ...
alt Propiedades de configuración de un Widget Page

Extendiendo las utilidades

Aparte de las utilidades que Tailwind trae por defecto, nosotros podemos crear nuevas clases dependiendo de nuestras necesidades. En este caso necesitamos un ancho fijo para nuestro campo de búsqueda, así que iremos a nuestro archivo tailwind.css, en la raíz de nuestro proyecto, y añadimos la siguiente clase CSS:

tailwind.css
.search-input-width {
  width: 215px;
  @apply border rounded bg-gray-100;
}

Verás que estamos utilizando @apply, palabra reservada utilizada en PostCSS y que se aprovecha para trabajar en Tailwind, pero no te preocupes ahora por ella. Veremos de que trata en el siguiente apartado.

También vamos a querer que, al hacer focus en el campo de búsqueda, el cursor para escribir texto aparezca a la izquierda del campo. Para eso usaremos los @variants. Tailwind nos provee de inicio de algunos @variants como por ejemplo responsive, hover, focus, active y group-hover. Para establecer un @variants lo hacemos de la siguiente forma:

tailwind.css
@variants focus {
  .search-input-width {
    text-align: left;
  }
}

Pero, ¿que pasa si queremos un @variants para el selector de placeholder? Deberemos construir nuestro propio @variants creando un plugin en el archivo de configuración de Tailwind. Vamos a nuestro archivo tailwind-config.js e introducimos lo siguiente en el array de plugins:

tailwind-config.js:
plugins: [  
    function ({ addVariant, e }) {
      addVariant('placeholder', ({ modifySelectors, separator }) => {
        modifySelectors(({ className }) => {
          return `.${e(`placeholder${separator}${className}`)}::placeholder`
        })
      })
    }
  ]

Ahora nuestro @variants del placeholder esta listo para poder usarse tal como te muestro en el snippet de aquí abajo. Podremos escribir nuestros estilos para el placeholder y después transpilar nuestro código con el comando npm run tw.

tailwind.css
@variants placeholder {
  .search-input-width {
    text-align: center;
  }
}

Y aplicamos la clase placeholder:search-input-width en nuestro header.component.html:

<div class="w-1/3 xs:hidden sm:hidden md:hidden xl:flex justify-center items-center px-24">  
      <input
        class="search-input-width focus:search-input-width placeholder:search-input-width"
        type="text"
        name="search"
        id="search"
        placeholder="Busca"
        autocomplete="off"
      />
</div>  

Para que los cambios en nuestro archivo tailwind.css surtan efecto debemos volver a compilar con el comando npm run build.

Ahora pasemos a crear nuestro siguiente componente al que llamaremos profile y para eso ejecutaremos en nuestro terminal el comando ng g c profile

Acto seguido incluimos el componente en nuestro archivo raíz app.component.html:

<div class="bg-gray-100 h-screen">  
  <app-header></app-header>
  <app-profile></app-profile>
</div>

<router-outlet></router-outlet>  

Nuestro archivo profile.component.html contendrá la siguiente estructura HTML:

<section class="bg-gray-100">  
  <div class="flex photos-container mx-auto sm:pt-6 sm:pb-2 md:pt-16 md:pb-12">
    <div
      class="flex md:w-1/3 xl:w-1/3 xs:w-auto sm:w-auto xs:justify-start sm:justify-start xs:mx-6 sm:mx-6 md:justify-center xl:justify-center"
    >
      <img
        class="rounded-full border xs:w-20 xs:h-20 sm:w-20 sm:h-20 md:w-40 md:h-40 xl:w-40 xl:h-40"
        src="../../assets/images/atgram_profile.jpg"
      />
    </div>
    <div class="w-2/3">
      <div class="items-center mb-4 xs:hidden sm:hidden md:flex xl:flex">
        <h2 class="text-3xl font-thin inline-block mr-2">atsistemas</h2>
        <div class="flex items-center">
          <button
            class="bg-blue-500 mr-4 w-20 h-6 px-2 text-sm font-semibold rounded-sm text-white"
          >
            Seguir
          </button>
          <button class="bg-blue-500 mr-4 w-10 h-6 rounded-sm text-white">
            <div
              class="mx-auto"
              style="background-image: url('./assets/images/instagram_icons_2.png'); height: 6px; width: 9px; background-position: -327px -213px"
            ></div>
          </button>
        </div>
        <button>···</button>
      </div>
      <div class="flex items-center mb-4 flex-wrap md:hidden xl:hidden">
        <div class="w-full">
          <h2 class="text-3xl font-thin inline-block mr-2">atsistemas</h2>
          <button>···</button>
        </div>

        <div class="flex items-center">
          <button
            class="bg-blue-500 mr-4 w-20 h-6 px-2 text-sm font-semibold rounded-sm text-white"
          >
            Seguir
          </button>
          <button class="bg-blue-500 mr-4 w-10 h-6 rounded-sm text-white">
            <div
              class="mx-auto"
              style="background-image: url('./assets/images/instagram_icons_2.png'); height: 6px; width: 9px; background-position: -327px -213px;"
            ></div>
          </button>
        </div>
      </div>

      <div class="flex mb-4 xs:hidden sm:hidden md:flex xl:flex">
        <h3 class="mr-4"><span class="font-bold">180</span> publicaciones</h3>
        <h3 class="mr-4"><span class="font-bold">293</span> seguidores</h3>
        <h3 class="mr-4"><span class="font-bold">137</span> seguidos</h3>
      </div>

      <div class="xs:hidden sm:hidden md:block xl:block">
        <p class="font-bold">atSistemas</p>
        <p>
          👉Más de 1200 personas apasionadas por la
          <a class="text-indigo-800 underline" href="#"
            >#TransformaciónDigital</a
          >, <a class="text-indigo-800 underline" href="#">#talento</a> e
          <a class="text-indigo-800 underline" href="#">#innovación</a> | Somos
          <a class="text-indigo-800 underline" href="#">#atSistemas</a> 👈
        </p>
        <p>www.atsistemas.com</p>
      </div>
    </div>
  </div>

  <div class="m-6 text-sm xs:block sm:block md:hidden xl:hidden">
    <p class="font-bold">atSistemas</p>
    <p>
      👉Más de 1200 personas apasionadas por la
      <a href="#">#TransformaciónDigital</a>, <a href="#">#talento</a> e
      <a href="#">#innovación</a> | Somos <a href="#">#atSistemas</a> 👈
    </p>
    <p>www.atsistemas.com</p>
  </div>

  <div class="photos-container mx-auto">
    <ul class="flex flex-row  md:my-4 lg:my-8">
      <li class="m-5">
        <img
          class="rounded-full border mb-2 xs:w-16 xs:h-16 sm:w-16 sm:h-16 md:w-24 md:h-24"
          src="../../assets/images/atgram_digital_wolves.jpg"
          alt="Foto del perfil de Digital Wolves"
        />
        <div
          class="w-24 md:text-center sm:text-left xs:text-sm sm:text-sm xs:font-medium sm:font-medium md:font-semibold overflow-hidden truncate"
        >
          Digital Wolves
        </div>
      </li>
      <li class="m-5">
        <img
          class="rounded-full border mb-2 xs:w-16 xs:h-16 sm:w-16 sm:h-16 md:w-24 md:h-24"
          src="../../assets/images/atgram_linkedin.jpg"
          alt="Foto del perfil de LinkedIn"
        />
        <div
          class="w-24 md:text-center sm:text-left xs:text-sm sm:text-sm xs:font-medium sm:font-medium md:font-semibold overflow-hidden truncate"
        >
          LinkedIn
        </div>
      </li>
      <li class="m-5">
        <img
          class="rounded-full border mb-2 xs:w-16 xs:h-16 sm:w-16 sm:h-16 md:w-24 md:h-24"
          src="../../assets/images/atgram_twitter.jpg"
          alt="Foto del perfil de Twitter"
        />
        <div
          class="w-24 md:text-center sm:text-left xs:text-sm sm:text-sm xs:font-medium sm:font-medium md:font-semibold overflow-hidden truncate"
        >
          Twitter
        </div>
      </li>
    </ul>
  </div>
  <div class="photos-container mx-auto">
    <div
      class="flex justify-center border-t border-gray-300 text-sm font-medium"
    >
      <ul class="flex">
        <li
          class="flex items-center uppercase mx-8 py-4  border-t border-black"
        >
          <div
            class="bg-no-repeat"
            style="background-image: url('./assets/images/instagram_icons_2.png'); background-position: -274px -337px; height: 12px; width: 12px;"
          ></div>
          <div class="ml-2">Publicaciones</div>
        </li>
        <li class="flex items-center uppercase mx-8 py-4">
          <div
            class="bg-no-repeat"
            style="background-image: url('./assets/images/instagram_icons_2.png'); background-position: -246px -337px; height: 12px; width: 12px;"
          ></div>
          <div class="ml-2">Etiquetadas</div>
        </li>
      </ul>
    </div>
  </div>
</section>

alt

Creando componentes

Como os habréis dado cuenta a estas alturas del artículo la cantidad de clases que forman un componente puede ser enorme y en muchas ocasiones tenemos elementos HTML que utilizan las mismas clases de Tailwind una y otra vez. Para evitar esto tenemos la componentización.

Esto nos permite pasar de tener algo así:

profile.component.html
...
<img class="rounded-full border mb-2 xs:w-16 xs:h-16 sm:w-16 sm:h-16 md:w-24 md:h-24"/>  
...

A tener algo como esto:

profile.component.html
...
<img class="stories-image"/>  
...

Lo que vamos a hacer para conseguir esto es ir a nuestro archivo tailwind.css y escribiremos lo siguiente:

tailwind.css
.stories-image {
  @apply rounded-full border mb-2;
}

Ahora recuerda volver a transpilar el archivo CSS con la ejecución del comando npm run tw.

Y te preguntarás, ¿qué pasa con el responsive? Pues bien, para eso tenemos que usar @screen para cada uno de los breakpoint de nuestra aplicación por lo tanto quedaría de la siguiente manera:

tailwind.css
@screen xs {
  .stories-image {
    @apply w-16 h-16;
  }
}

@screen sm {
  .stories-image {
    @apply w-16 h-16;
  }
}

@screen md {
  .stories-image {
    @apply w-24 h-24;
  }
}

Como veis le estamos diciendo que en tamaños de pantalla muy pequeños utilice w-16 y h-16 y a partir de tamaños medianos en adelante usamos w-24 y h-24.

Para continuar con nuestra interfaz de AtGram, vamos a generar un nuevo componente llamado photos. Para eso vamos a la terminal y escribimos ng g c photos, acto seguido vamos a la carpeta recién generada photos, abrimos el archivo photos.component.html y escribimos el siguiente código:

photos.component.html
<section class="bg-gray-100">  
  <div class="flex photos-container mx-auto flex-wrap">
    <div class="w-1/3 xs:p-1 sm:p-1 md:p-2">
      <img
        src="../../assets/images/atgram_blockchain.jpg"
      />
    </div>
    <div class="w-1/3 xs:p-1 sm:p-1 md:p-2">
      <img
        src="../../assets/images/atgram_blockchain.jpg"
      />
    </div>
    <div class="w-1/3 xs:p-1 sm:p-1 md:p-2">
      <img
        src="../../assets/images/atgram_blockchain.jpg"
      />
    </div>
    <div class="w-1/3 xs:p-1 sm:p-1 md:p-2">
      <img
        src="../../assets/images/atgram_blockchain.jpg"
      />
    </div>
    <div class="w-1/3 xs:p-1 sm:p-1 md:p-2">
      <img
        src="../../assets/images/atgram_blockchain.jpg"
      />
    </div>
    <div class="w-1/3 xs:p-1 sm:p-1 md:p-2">
      <img
        src="../../assets/images/atgram_blockchain.jpg"
      />
    </div>
  </div>
</section>  

Después incluimos el componente en nuestro archivo app.component.html.

<div class="bg-gray-100 h-screen">  
  <app-header></app-header>
  <app-profile></app-profile>
  <app-photos></app-photos>
</div>

<router-outlet></router-outlet>  

alt

Pasemos ahora a crear el footer de la aplicación. Para ello vamos a crear nuestro nuevo componente con el comando ng g c footer, seguidamente vamos al archivo footer/footer.component.html y añadimos el siguiente código HTML:

footer.component.html
<footer class="bg-gray-100">  
  <div class="photos-container mx-auto">
    <ul
      class="flex flex-wrap xs:justify-center sm:justify-center py-8 uppercase text-xs text-indigo-800 font-medium"
    >
      <li class="mr-6">Información</li>
      <li class="mr-6">Asistencia</li>
      <li class="mr-6">Prensa</li>
      <li class="mr-6">API</li>
      <li class="mr-6">Empleo</li>
      <li class="mr-6">Privacidad</li>
      <li class="mr-6">Condiciones</li>
      <li class="mr-6">Directorio</li>
      <li class="mr-6">Perfiles</li>
      <li class="mr-6">Hastags</li>
      <li class="mr-6">Idioma</li>
      <li class="text-gray-600">© 2019 AtGram</li>
    </ul>
  </div>
</footer>  

Y añadimos nuestro nuevo componente dentro del archivo raíz app.component.html.

<div class="bg-gray-100 h-screen">  
  <app-header></app-header>
  <app-profile></app-profile>
  <app-photos></app-photos>
  <app-footer></app-footer>
</div>

<router-outlet></router-outlet>  

alt

Bonus

Para terminar de pulir nuestra aplicación vamos a crear mas clases con la intención de hacer mas reutilizable nuestro código, por lo tanto vamos a crear diferentes componentes de Tailwind.

En nuestra cabecera se están repitiendo las clases de los iconos que van a la derecha por lo tanto vamos a agruparlos en una sola clase:

tailwind.css
.header-icons {
  background-image: url('./assets/images/instagram_icons.png'); 
  height: 24px; 
  width: 24px;
  @apply bg-no-repeat mx-4;
}

En nuestro HTML pasaremos de algo como esto:

header.component.html
<div class="bg-no-repeat mx-4"  
    id="atgram-explore"
    style="background-image: url('./assets/images/instagram_icons.png'); background- 
    position: -257px -200px; height: 24px; width: 24px;">
</div>  

A esto, en cada uno de los iconos:

header.component.html
<div class="header-icons"  
   id="atgram-explore"
   style="background-position: -257px -200px;">
</div>  

Te dejo como reto que encuentres otros patrones de clases que se están repitiendo y tu mismo crees los componentes para cada uno de ellos.

Finalmente vamos a realizar la parte que hace que el header se quede fijo una vez empiezas a hacer scroll en la página. Para eso necesitamos tener en nuestro header el evento window:scroll y un template tag de Angular en el container del header.

header.component.html
...
<header  
  (window:scroll)="stickOnScroll($event)"
  class="sticky top-0 bg-white"
>
  <div #container class="md:container xl:container m-auto flex h-20">
...

En nuestro archivo header.component.ts incluiremos el siguiente código:

header.component.ts
import {  
  Component,
  OnInit,
  HostListener,
  ElementRef,
  ViewChild
} from '@angular/core';

@Component({
  selector: "app-header",
  templateUrl: "./header.component.html",
  styleUrls: ["./header.component.css"]
})
export class HeaderComponent implements OnInit {  
  @ViewChild("atgramLogo") atgramLogo: ElementRef;
  @ViewChild("slash") slash: ElementRef;
  @ViewChild("container") container: ElementRef;
  constructor() {}

  ngOnInit() {}

  @HostListener("scroll", ["$event"])
  stickOnScroll($event) {
    const scrollOffset = $event.target.children[0].scrollTop;
    if (scrollOffset !== 0) {
      this.atgramLogo.nativeElement.style.display = "none";
      this.slash.nativeElement.style.display = "none";
      this.container.nativeElement.style.height = "80px";
    } else {
      this.atgramLogo.nativeElement.style.display = "block";
      this.slash.nativeElement.style.display = "block";
      this.container.nativeElement.style.height = "100px";
    }
  }
}

Con esto al hacer scroll de la página hacia abajo veremos que el header se mantiene pegado en la parte superior de la página y habremos finalizado nuestra app de AtGram. Puedes ver todo el código del ejercicio del post aquí

Conclusiones

En resumen, como puedes ver con Tailwind puedes maquetar interfaces web de forma rápida y sencilla. Tailwind nos provee de todas las herramientas necesarias para escribir CSS de forma fácil y podemos extender la utilidad tanto como necesitemos. Construye tus componentes de forma fácil y vuelve a enamorarte de maquetar como lo hiciste la primera vez.

Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevas entregas!

Jesús Mur
Autor

Jesús Mur

Desarrollador frontend en atSistemas. Me encanta trabajar con CSS. Hago vídeos de YouTube sobre programación. YouTube: Jesus Mur Twitter: @JesusMurF