Novo Livro!
Categorias
Projetos
CSS

Como animar border-image em CSS

Como animar border-image em CSS

Bordas que se desenham sozinhas no hover, giram como um ponteiro de relógio ou se quebram em fatias repetidas. Todos esses efeitos saem de border-image, uma propriedade que existe há mais de uma década e quase ninguém usa, sem uma única linha de JavaScript.

O resumo é que dá para usar uma imagem ou um gradiente como borda, em vez dos estilos padrão (e um tanto sem graça) como solid ou dashed. A ideia aqui é ir além disso e mostrar como animar imagens de borda para criar interfaces bem mais interessantes.

Assim:

A border-image é uma daquelas propriedades que a gente toma como garantidas. E ela não é nova: Andy Clarke tem um artigo relativamente recente que revisita a propriedade e mostra o quanto é possível ser criativo com ela. Vale a leitura.

Por que usar imagens de borda?

Existe uma ressalva grande nas imagens de borda: elas não acompanham a curvatura das bordas. É uma limitação séria para uma animação que percorre o elemento inteiro, como a que será feita aqui. Mas há como contornar isso neste exemplo.

Só que isso levanta a pergunta: “por que contornar a limitação se existem outras abordagens?” Basta olhar a abordagem do Temani Afif, que usa uma máscara CSS (ele escreveu um texto excelente sobre isso):

A border-image é interessante porque é bem eficiente, sobretudo quando o assunto é animar imagens de borda e gradientes. Isso acontece por dois motivos.

O primeiro é que replicar o desenho em todas as bordas é fácil (e automático). O segundo é que dá para “fatiar” as imagens com a propriedade border-image-slice.

Ela funciona de forma parecida com background-size e background-position, onde uma única fatia da imagem pode ser aproveitada. A diferença é que as fatias da imagem de borda podem percorrer as bordas inteiras, e animar isso cria efeitos bonitos.

O HTML

Nada de mais por aqui: apenas uma div com um pouco de texto formatado dentro:

<div class=card><strong>Bruce Wayne</strong></div>

E a imagem? Ela entra como background em CSS, então vamos direto para lá.

Estilos base

O .card recebe dimensões definidas e uma imagem de fundo:

.card {
width: 150px;
aspect-ratio: 0.69;
position: relative;
background: center/90% no-repeat;
background-image: url("batman.jpg");
}

Também vale um estilo no texto, para garantir que ele não fique exatamente sobre o fundo:

A imagem de borda

Aqui será usado um gradiente CSS de uma cor só, por dois motivos: primeiro porque ele é um tipo de imagem de fundo, e segundo porque fica mais fácil demonstrar a animação.

Todos os valores de border-image também serão definidos em suas próprias propriedades longhand, para deixar claro qual valor afeta o gradiente de qual maneira.

Começando pelo efeito de um linear-gradient simples:

.card {
/* ... */
/* Cria uma cor linear */
border-image-source: linear-gradient(-45deg, red 0%, transparent 0%);
/* Controla como o gradiente é fatiado */
border-image-slice: 1;
/* Define a espessura física da borda */
border-image-width: 5px;
}

Talvez você tenha notado na demonstração que existe uma pequena folga entre a borda animada e a imagem. Dá para empurrar a borda para fora do .card com a propriedade border-image-outset:

.card {
/* ... */
border-image-source: linear-gradient(-45deg, red 0%, transparent 0%);
border-image-slice: 1;
border-image-width: 5px;
/* Empurra a borda para fora da div */
border-image-outset: 5px;
}

São necessárias border-image-source e border-image-width para renderizar a borda. No início, a fonte da imagem (o gradiente) é totalmente transparente. Como red e transparent estão ambas posicionadas em 0%, o navegador não tem espaço para misturar as cores.

E como transparent vem em segundo lugar no código, é ela que assume a posição inicial em 0% e preenche o resto do gradiente. O border-image-slice: 1 cria fatias de 1px do gradiente, o que gera um preenchimento suave ao longo de toda a borda.

Animando a imagem de borda

Há outro detalhe importante sobre gradientes, especialmente sobre animá-los: por padrão, não dá. Fazer a transição (ou interpolação) de uma cor para outra é difícil, porque na prática se está transicionando os pontos inicial e final de um gradiente, que são porcentagens.

É aí que entra o registro de uma custom property, já que variáveis CSS podem, sim, ser animadas:

@property --p {
syntax: "<percentage>";
initial-value: 0%;
inherits: false;
}

E dá para usá-la no gradiente:

.card {
/* ... */
border-image-source: linear-gradient(-45deg, red var(--p), transparent 0%);
}

Agora, digamos que --p deva transicionar do valor padrão de 0% para 100% quando o .card receber hover:

.card {
/* ... */
border-image-source: linear-gradient(-45deg, red var(--p), transparent 0%);
/* ... */
&:hover {
--p: 100%;
}
}

Isso diz ao navegador que o vermelho deve terminar a transição na marca de 100% (o fim da linha). O gradiente cresce até virar um vermelho sólido, como se a borda estivesse se desenhando:

Variações

O exemplo anterior já seria um bom lugar para parar, mas há muito mais a fazer. Dá para mudar a direção, acelerar, desacelerar, parar em outras posições ou usar tipos diferentes de gradiente.

Vamos tentar um conic-gradient em vez de um linear. É possível animar o valor de border-image-slice e usar border-image-repeat: round para repetir as fatias dentro das bordas.

O empilhamento round tenta encaixar um número inteiro de repetições na borda e, quando não consegue, a imagem estica ou comprime um pouco até caber.

.card {
/* Cria uma roda de cores nítida e expansível usando a variável de ângulo */
border-image-source: conic-gradient(from 0deg, red 0deg, transparent 0%);
/* Repete as regiões fatiadas suavemente, sem cortar */
border-image-repeat: round;
border-image-width: 5px;
border-image-slice: 1;
}

Ainda é preciso registrar custom properties para movimentar o gradiente, desta vez para transicionar o valor de border-image-slice (chamado aqui de --n) e o ângulo do conic-gradient (chamado de --a):

Nota: também é possível animar border-image-slice diretamente, sem passar por uma custom property registrada. O resultado varia um pouco conforme a forma como o navegador lida com essas mudanças.

/* Registra uma custom property para o border-image-slice */
@property --n {
syntax: "<number>";
initial-value: 1;
inherits: false;
}
/* Registra uma custom property para o ângulo do gradiente */
@property --a {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}

Agora, jogando essas variáveis no CSS original:

.card {
border-image-source: conic-gradient(from var(--a), red var(--a), transparent 0%);
border-image-width: 5px;
border-image-slice: var(--n);
border-image-repeat: round;
}

…e dizendo ao CSS exatamente quais propriedades estão em transição, o que é bom para a performance:

.card {
border-image-source: conic-gradient(from var(--a), red var(--a), transparent 0%);
border-image-width: 5px;
border-image-slice: var(--n);
border-image-repeat: round;
transition-property: --n, --a;
transition-duration: 0.6s;
}

…e fazendo as propriedades transicionarem no hover:

.card {
border-image-source: conic-gradient(from var(--a), red var(--a), transparent 0%);
border-image-width: 5px;
border-image-slice: var(--n);
border-image-repeat: round;
transition-property: --n,--a;
transition-duration: 0.6s;
&:hover {
/* Aumenta a profundidade do fatiamento para repetir o padrão da borda */
--n: 20;
/* Gira o gradiente uma volta completa para desenhar a borda */
--a: 360deg;
}
}

O valor da fatia (--n) começa em 1 e sobe até 20 no hover. Isso significa que a fatia fica mais grossa (até 20px), o que dá para perceber pelas quebras que aparecem nas bordas.

Mais exemplos

Como dito antes, há muita coisa possível com imagens de borda animadas: animar várias cores, usar gradientes repetidos e por aí vai. Aqui estão alguns exemplos reunidos.

A lição de casa fica sendo destrinchar cada um desses exemplos e criar novos efeitos a partir deles. Se a animação de borda for o próximo passo do seu repertório, vale conferir também 10 dicas para melhores animações e transições CSS e a nova propriedade CSS border-shape.