Houdini CSS: um jeito mágico de criar estilos personalizados
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:
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?
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:
- Parsing: conversão e leitura do código HTML, CSS e JavaScript
- Style: o navegador aplica os estilos a cada elemento
- Layout: o navegador recalcula o layout da página
- Paint: o navegador pinta os píxeis na tela
- Composite: imagem final da tela é exibida para a pessoa usuária
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 atributostyle
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ê.
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:
O uso desta API é semelhante ao Paint API, com a seguinte sequência:
- 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) elayout
(função que executa um layout); - Importar a função dentro da tag
<script>
no interior da tag<head>
no HTML através a funçãoaddModule
usando oCSS.layoutWorklet.addModule()
; - 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.
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: