Houdini CSS: um jeito mágico de criar estilos personalizados

Houdini CSS: um jeito mágico de criar estilos personalizados
RODRIGO SILVA HARDER
RODRIGO SILVA HARDER

Compartilhe

Suponha que você é uma pessoa desenvolvedora e precisa implementar um border-radius invertido em um botão, de modo que as bordas sejam arredondadas para dentro ao invés de para fora, da seguinte maneira:

Imagem de um botão com a borda arredondada para dentro.

Para conseguir esse efeito pensaríamos na possibilidade de usar a propriedade border-radius com valores negativos, como mostrado no trecho de código abaixo:

See the Pen border-radius invertido by Rodrigo Harder (@Rodrigo-Harder) on CodePen.

No entanto, como observado anteriormente, o formato do quadrado não se altera com valores negativos e o resultado não é o esperado.

Sendo assim, percebemos que as propriedades do CSS, podem ser limitadas para casos mais complexos.

Por causa disso, surgiu uma alternativa que possibilita expandir as possibilidades de estilo dessa linguagem, o Houdini CSS.

Portanto, neste artigo vamos explorar o que é o Houdini, como ele funciona, suas vantagens, algumas API's que fazem parte dele, exemplos em código e a compatibilidade e suporte em diferentes navegadores.

Preparado para desvendar o Houdini CSS e expandir a criatividade para criar recursos incríveis?

O que é o Houdini CSS?

Gif do personagem Michael Scott da série The Office realizando um truque de mágica.

O Houdini CSS não é nem a música da Dua Lipa, muito menos o grande ilusionista Harry Houdini, mas, se trata de um conjunto de API's que permite criar mecanismos de renderização CSS ao combinar as principais linguagens do Front-End (HTML, CSS e JavaScript).

Isso possibilita estender os recursos do navegador e ir além do que pode ser feito apenas com o CSS padrão.

Um dos recursos mais importantes para essa expansão são os worklets, versões leves dos Web Workers, conhecidos por permitir que alguns códigos rodem em segundo plano, melhorando a interface.

Os worklets, acessam partes fundamentais do processo de renderização do navegador e executam código de alto desempenho sem necessidade de pré-processadores ou frameworks complexos.

JavaScript vs Houdini

Agora que sabemos o que é o Houdini, podemos nos perguntar, mas porque não resolver essas limitações do CSS com o JavaScript puro?

Bom, para responder essa pergunta precisamos entender como ambas as opções interagem com o pixel pipeline, ou seja, a sequência que os navegadores seguem para transformar o código de uma página web (HTML, CSS e JavaScript) em píxeis visíveis na tela.

Há algumas partes que compõem a pixel pipeline, entre elas:

  1. Parsing: conversão e leitura do código HTML, CSS e JavaScript
  2. Style: o navegador aplica os estilos a cada elemento
  3. Layout: o navegador recalcula o layout da página
  4. Paint: o navegador pinta os píxeis na tela
  5. Composite: imagem final da tela é exibida para a pessoa usuária
Imagem de um pixel pipeline com a seguinte sequência: Parsing, Style, Layout, Paint e Composite.

Ao usar JavaScript para alterar os estilos da página, pode ser necessário repetir várias vezes uma mesma etapa, gerando recálculos de estilos, layouts e pintura, o que reduz o desempenho da aplicação, principalmente ao lidar com animações e layouts complexos.

Por outro lado, o Houdini, modifica etapas específicas da pipeline, dessa forma, não há risco de sobrecarregar o DOM com manipulações constantes.

Por exemplo, com a Paint API, é possível definir como um elemento vai ser desenhado diretamente na etapa de pintura, sem interferir em etapas anteriores. Por isso, essa solução é mais performática e eficiente, por otimizar o desempenho da aplicação.

Principais API's do Houdini

A partir daqui vamos conhecer algumas API's do Houdini e como utilizá-las em diversos contextos.

Typed Object Model API

Antes do Houdini, a única forma do JavaScript interagir com o CSS era analisando e modificando valores CSS como strings. Por exemplo, para alterar o tamanho da fonte através do JavaScript era necessário realizar a concatenação de strings, da seguinte maneira:

<head>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1 id="exemplo">Exemplo de Typed Object Model API</h1>
    <script src="script.js"></script>
</body>
#exemplo{
    font-size: 20px;
}
//Seleciona o elemento do DOM que queremos alterar
var exemploTypedOM = document.querySelector("#exemplo")

//Cria uma variável e armazena o valor 20, lido como string pelo JavaScript
var newFontSize = 70

//Seleciona o estilo de tamanho de fonte do elemento HTML e armazena a concatenação das strings
var novoEstilo = exemploTypedOM.style.fontSize = newFontSize + "px"; 

//Exibe o resultado no console do navegador
console.log(novoEstilo); 

Esta API, representa os valores CSS como objetos JavaScript tipados, por conta disso, o código pode ser manipulado de maneira mais fácil e confiável, aumentando a performance durante a execução. Os valores CSS são representados pela interface CSSUnitValue que consiste em um valor e uma propriedade de unidade.

{
  value: 2, 
  unit: "em"
}

Além disso, essa interface pode ser usada com algumas propriedades como:

  • computedStyleMap(): permite ler os valores das propriedades aplicadas a um elemento.
  • attributeStyleMap: define valores de estilos inline, ou seja, sobrescreve os estilos do arquivo e define diretamente no atributo style do elemento HTML novos estilos.

Devido a estas vantagens, atualmente, a alteração é feita como mostrado no código abaixo:

//Seleciona o elemento do DOM que queremos alterar
var exemploTypedOM = document.querySelector("#exemplo")

// Pega o valor inicial atribuído no CSS padrão
exemploTypedOM.computedStyleMap().get("font-size");

// Atribui novos valores de estilo para o tamanho da fonte
exemploTypedOM.attributeStyleMap.set("font-size", CSS.em(2));

// Aplica o novo estilo
var novoEstilo = exemploTypedOM.attributeStyleMap.get("font-size")

// Visualiza o objeto criado no console do navegador
console.log(novoEstilo)

Properties and Values API

A API Properties and Values permite definir propriedades personalizadas do CSS, como verificação do tipo de propriedade, valores padrão e propriedades que herdam ou não seus valores.

Isso pode ser feito através do método registerProperty, que permite registrar novas propriedades CSS customizadas que se comportam como propriedades CSS nativas.

Por exemplo, para criar a variável cor-animada, inicialmente, é preciso definir no arquivo JavaScript as seguintes propriedades:

  • name: nomeia a propriedade personalizada;
  • syntax: define a sintaxe esperada para o valor da propriedade, usando propriedades CSS nativas, como, <color>;
  • inherits: informa se a propriedade deve ser herdada pelos elementos filhos;
  • initialValue: define o valor inicial da propriedade, além de ser aplicado em caso de erro.

O código ficaria assim para a variável --cor-animada:

CSS.registerProperty({
    name: '--cor-animada',
    syntax: '<color>',
    initialValue: 'black',
    inherits: false
});

Para aplicar esta variável em um contexto simples, podemos nos basear no seguinte código HTML e CSS:

<head>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="caixa"></div>
    <script src="script.js"></script>
</body>
/*Criação das variáveis de cor*/
:root {
    --cor-a: red;
    --cor-b: blue;
}

/*Definição do tamanho da caixa e aplicação da animação e cor de fundo*/
.caixa {
    width: 300px;
    height: 300px;
    animation: 3s mudaCor infinite alternate linear;
    background-color: var(--cor-animada)
}

/*Criação da animação*/
@keyframes mudaCor {
    from {
        --cor-animada: var(--cor-a);
    }

    to {
        --cor-animada: var(--cor-b);
    }
}

A variável --cor-animadaé aplicada na animação mudaCor e recebe como parâmetro duas cores pré-definidas no :root. O resultado é uma animação que foi do azul para o vermelho num degradê.

Gif da animação da cor de preenchimento de um quadrado da cor vermelha para a azul.

Paint API

Lembram-se do exemplo lá do começo deste artigo sobre o border-radius invertido? Bom, agora detalharemos a solução deste problema que foi proposta inicialmente pelo Mario Souto.

Mas primeiro, vamos entender mais sobre esta API. Com a Paint API, é possível desenhar na borda, conteúdo ou fundo de um elemento aplicando o mesmo conceito usado na API do Canvas do HTML5.

Para usá-la, é necessário inicialmente registrar um worklet de pintura por meio do comando registerPaint.

Ele precisa de dois parâmetros: o nome da propriedade e a classe que define a lógica do que será desenhado na tela. A lógica segue abaixo:

//Registrando a classe com um nome para usarmos na propriedade background-image
registerPaint('border-radius-invertido', class BorderRadiusInvertidoPainter {

    // Define as propriedades CSS que serão observadas e usadas pelo paint
    static get inputProperties() {
        return ['--border-radius-invertido', '--background-color'];
    }

    // Método que desenha e apaga um círculo em uma posição específica
    circulo(context, x, y, radius) {

        // Salva o estado atual do contexto
        context.save();

        // Inicia um novo caminho
        context.beginPath();

        // Desenha um arco (círculo completo) no caminho
        context.arc(x, y, radius, 0, 2 * Math.PI);

        // Define a região de corte como o caminho atual (círculo)
        context.clip();

        // Limpa a área dentro do círculo desenhado
        context.clearRect(x - radius, y - radius, radius * 2, radius * 2);

        // Restaura o contexto ao estado salvo
        context.restore();
    }

    // Método principal que desenha o elemento no contexto do canvas
    paint(ctx, geom, props) {

        //Converte o valor da propriedade --border-radius-reverse para um número
        const valorDoArredondamento = parseFloat(props.get('--border-radius-invertido'));

        // Obtém o valor da propriedade --background-color
        const corDeFundo = props.get('--background-color').toString();

        // Define a cor de preenchimento
        ctx.fillStyle = corDeFundo;

        // Preenche o retângulo com a cor de fundo
        ctx.fillRect(0, 0, geom.width, geom.height);

        // Define as coordenadas dos quatro cantos do retângulo
        const vertices = [
            [0, 0],
            [geom.width, geom.height],
            [0, geom.height],
            [geom.width, 0] 
        ];

        // Para cada canto, chama o método clearCircle
        vertices.forEach(([x, y]) => this.circulo(ctx, x, y, valorDoArredondamento));
    }
});

Após criar o worklet deve-se incorporá-lo ao arquivo HTML dentro da tag <script> no interior da tag <header>, por meio do CSS.paintWorklet.addModule(). Ficaria assim:

<head>
    <link rel="stylesheet" href="style.css">
    <script>
        CSS.paintWorklet.addModule('script.js');
    </script>
</head>
<body>
    <div class="radius-invertido"></div>
</body>

Em seguida, aplica-se o border-radius-invertido na propriedade background-image através da função paint e por fim ela é usada como uma variável com o valor de 60px.

.radius-invertido {
    width: 300px;
    height: 300px;
    display: inline-block;
    background-color: transparent;
    --background-color: blue;
    background-image: paint(border-radius-invertido);
    --border-radius-invertido: 60;
}

Layout API

Geralmente em um projeto, utilizam-se formatos de display como: flexbox, grid, inline, inline-block e block para definir o layout das páginas e como os elementos serão distribuídos na tela.

Contudo, por meio da API de Layout é possível expandir essas possibilidades e criar novos formatos de display.

Um exemplo clássico de uso é o Masonry, que está presente em sites e aplicativos como, Instagram, Pinterest, Tumblr, Behance, entre outros.

Esse formato de layout personalizado, permite criar um mosaico com os itens de uma seção, como mostrado na imagem abaixo:

Imagem da aplicação do layout Masonry, com criação de um mosaico com 12 quadrados coloridos e de tamanhos diferentes em três colunas.

O uso desta API é semelhante ao Paint API, com a seguinte sequência:

  1. Registar um worklet por meio do registerLayout que deve conter alguns métodos principais como, inputProperties (definir as propriedades que o worklet deve aplicar), intrisicSizes(define como um bloco ou seu conteúdo deve se comportar em um layout) e layout (função que executa um layout);
  2. Importar a função dentro da tag <script> no interior da tag <head> no HTML através a função addModule usando o CSS.layoutWorklet.addModule();
  3. Usar no arquivo CSS dentro da propriedade display.

Para criar a imagem de mosaico com os quadrados coloridos, foi utilizado o seguinte código:

<head>
    <link rel="stylesheet" href="style.css">
    <script>
    CSS.layoutWorklet.addModule("https://cdn.jsdelivr.net/gh/GoogleChromeLabs/houdini-samples/layout-worklet/masonry/masonry.js");
    </script>
</head>
<body>
    <div style="background-color: #9400d3; width: 140px; height: 140px;"></div>
    <div style="background-color: #ff6347; width: 50px; height: 50px;"></div>
    <div style="background-color: #1e90ff; width: 160px; height: 160px;"></div>
    <div style="background-color: #4682b4; width: 60px; height: 60px;"></div>
    <div style="background-color: #8a2be2; width: 80px; height: 80px;"></div>
    <div style="background-color: #dc143c; width: 120px; height: 120px;"></div>
    <div style="background-color: #ff4500; width: 90px; height: 90px;"></div>
    <div style="background-color: #ffd700; width: 100px; height: 100px;"></div>
    <div style="background-color: #7fff00; width: 110px; height: 110px;"></div>
    <div style="background-color: #00ced1; width: 130px; height: 130px;"></div>
    <div style="background-color: #ff69b4; width: 150px; height: 150px;"></div>
    <div style="background-color: #32cd32; width: 70px; height: 70px;"></div>
</body>
body {
    width: 50vw;
    display: layout(masonry);
    --padding: 5;
    --columns: 3;
}

Na tag <script> o código do mosaico veio de um arquivo javascript já pronto, que está disponibilizado neste repositório do GitHub. No entanto, exploraremos o código de forma mais detalhada para entender o que está sendo usado no exemplo acima.

//Registrando um novo layout chamado 'masonry'
registerLayout('masonry', class {
  // Define as propriedades de entrada que o layout espera
  static get inputProperties() {
    // Espera as variáveis CSS --padding e --columns
    return ['--padding', '--columns'];
  }

  //Calcula os tamanhos intrínsecos do layout
  async intrinsicSizes() {
  }

  //Método principal que faz o layout dos filhos
  async layout(children, edges, constraints, styleMap) {

    //Tamanho disponível na direção inline (horizontal)
    const inlineSize = constraints.fixedInlineSize; 

    //Obtém o valor do padding a partir das propriedades de estilo
    const padding = parseInt(styleMap.get('--padding').toString());

    //Obtém o número de colunas (aceita `auto`)
    const columnValue = styleMap.get('--columns').toString();
    let columns = parseInt(columnValue);

    //Calcula o número de colunas automaticamente no caso do valor `auto` ou inválido
    if (columnValue == 'auto' || !columns) {

      //Define o tamanho médio da coluna
      columns = Math.ceil(inlineSize / 350); 
    }

    //Calcula o tamanho de cada coluna considerando o padding
    const childInlineSize = (inlineSize - ((columns + 1) * padding)) / columns;

    //Layout dos filhos, calculando a fragmentação de cada um
    const childFragments = await Promise.all(children.map((child) => {
      return child.layoutNextFragment({ fixedInlineSize: childInlineSize });
    }));

    //Tamanho total do bloco automático que será calculado
    let autoBlockSize = 0;

    //Inicializa um array para armazenar os deslocamentos das colunas
    const columnOffsets = Array(columns).fill(0);

    //Distribui os fragmentos dos filhos nas colunas
    for (let childFragment of childFragments) {

      //Encontra a coluna com o menor valor de deslocamento (menor altura)
      const min = columnOffsets.reduce((acc, val, idx) => {
        if (!acc || val < acc.val) {
          return { idx, val };
        }
        return acc;
      }, { val: +Infinity, idx: -1 });

      //Define os deslocamentos inline e block do fragmento do filho
      childFragment.inlineOffset = padding + (childInlineSize + padding) * min.idx;
      childFragment.blockOffset = padding + min.val;

      //Atualiza o deslocamento da coluna e o tamanho total do bloco automático
      columnOffsets[min.idx] = childFragment.blockOffset + childFragment.blockSize;
      autoBlockSize = Math.max(autoBlockSize, columnOffsets[min.idx] + padding);
    }

    // Retorna o tamanho total do bloco automático e os fragmentos dos filhos
    return { autoBlockSize, childFragments };
  }
});

De maneira geral, o worklet é registrado com o nome "masonry" e se comporta como um layout de colunas que organiza os elementos em formato de mosaico.

O método inputProperties define quais variáveis CSS o layout precisa, que são --padding e --columns.

O método intrinsicSizes: ainda não implementado, deve retornar os tamanhos intrínsecos do layout, o que ajuda a determinar o espaço necessário para o layout antes de aplicá-lo.

O método layout realiza a organização dos elementos filhos dentro do escopo da página através do cálculo do tamanho disponível para cada elemento filho e para o padding, determinando o número de colunas baseado em variável CSS ou no cálculo automático, calcula o tamanho de cada coluna levando em consideração o padding, distribui os elementos filhos nas colunas de maneira que cada coluna tenha aproximadamente o mesmo valor de altura e retorna o tamanho do bloco (autoBlockSize) e os fragmentos dos elementos filhos ajustados para o layout.

Compatibilidade e suporte nos navegadores

O suporte ao Houdini CSS está crescendo, mas ainda não é universal. Atualmente, navegadores como Chrome e Edge são os mais compatíveis para algumas API's.

Entretanto, caso queira usar alguma funcionalidade, e esteja em dúvida sobre a compatibilidade com o seu navegador, você pode usar o site Can I Use, onde é possível digitar a funcionalidade desejada no campo de busca e verificar o tipo de suporte disponível para a versão do navegador utilizado.

Suponha que você demonstrou interesse em usar a Paint API no Edge, basta digitar "Paint API" e realizar a busca.

Como resultado, você verá que na versão mais atual do Edge essa funcionalidade é suportada e pode ser usada sem problemas.

Gif mostrando como pesquisar uma funcionalidade no site Can I Use. Banner promocional da Alura, com um design futurista em tons de azul, apresentando o texto

Conclusão

Nossa, quanta coisa! Exploramos uma ferramenta poderosa que permite expandir o CSS tradicional na criação de estilos personalizados e mais complexos.

Entendemos como utilizar esse conjunto de API's disponibilizado pelo Houdini CSS e sua atuação na píxel pipeline, e por meio disso, compreendemos as vantagens de usar o Houdini ao invés do JavaScript puro na resolução de problemas de layout complexos.

Navegamos por algumas API's, como a Typed Object Model, Properties and Values, Paint e Layout, destacando sua funcionalidade e aplicabilidade com exemplos de código inseridos em problemáticas reais.

Por fim, abordamos a compatibilidade dessas API's em diferentes navegadores, e exploramos o site Can I Use para identificar o suporte de cada funcionalidade nas versões dos navegadores.

Alguns recursos do Houdini de fato ainda estão em estágio inicial, mas mostram-se um progresso enorme na busca por romper as barreiras do desenvolvimento de aplicações mais criativas e inovadoras.

Com certeza vai ser emocionante ver o que a comunidade de pessoas desenvolvedoras vai criar conforme o Houdini ganha força e melhor suporte aos navegadores!

Referências

Caso queira expandir seus conhecimentos sobre o Houdini e explorar mais sobre as API's que fazem parte dele, você pode consultar os links abaixo:

Se quiser explorar mais sobre CSS temos alguns conteúdos incríveis:

RODRIGO SILVA HARDER
RODRIGO SILVA HARDER

Graduado em Design Gráfico, Técnico em Química e Licenciatura em química, Rodrigo é apaixonado por ensinar e aprender e busca se aventurar em diferentes linguagens de programação. Faço parte da escola semente e atuo como monitor no time de Fórum Ops.

Veja outros artigos sobre Front-end