Como criar animações de texto dividido

Uma visão geral básica de como criar animações de letras e palavras divididas.

Neste post, quero compartilhar ideias sobre maneiras de resolver animações de texto divididas e interações para a Web que sejam mínimas, acessíveis e funcionem em todos os navegadores. Teste a demonstração.

Demo

Se preferir vídeos, confira a versão desta postagem no YouTube:

Visão geral

As animações de texto divididas podem ser incríveis. Vamos apenas arranhar a superfície do potencial de animação nesta postagem, mas ela fornece uma base para criar. O objetivo é animar de forma progressiva. O texto precisa ser legível por padrão, com a animação criada por cima. Os efeitos de movimento de texto dividido podem ficar extravagantes e potencialmente perturbadores. Por isso, só vamos manipular o HTML ou aplicar estilos de movimento se o usuário aceitar a animação.

Confira uma visão geral do fluxo de trabalho e dos resultados:

  1. Prepare variáveis condicionais de movimento reduzido para CSS e JS.
  2. Prepare utilitários de texto divididos em JavaScript.
  3. Orquestrar as condições e os utilitários no carregamento da página.
  4. Escreva transições e animações CSS para letras e palavras (a parte legal!).

Confira uma prévia dos resultados condicionais que vamos usar:

captura de tela dos desenvolvedores do Chrome com o painel "Elements" aberto e a redução de movimento definida como "reduce", e o h1 é mostrado sem divisão
O usuário prefere movimento reduzido: o texto é legível / não está dividido

Se um usuário preferir movimento reduzido, vamos deixar o documento HTML como está e não vamos fazer animações. Se o movimento estiver OK, vamos em frente e dividir em pedaços. Confira uma visualização do HTML depois que o JavaScript dividiu o texto por letra.

captura de tela dos desenvolvedores do Chrome com o painel "Elements" aberto e a redução de movimento definida como "reduce", e o h1 é mostrado sem divisão
O usuário aceita a animação. O texto é dividido em vários elementos <span>

Como preparar condicionais de movimento

A consulta de mídia @media (prefers-reduced-motion: reduce), disponível, será usada de forma conveniente no CSS e no JavaScript neste projeto. Essa consulta de mídia é nossa condição principal para decidir se o texto será dividido ou não. A consulta de mídia CSS será usada para reter transições e animações, enquanto a consulta de mídia JavaScript será usada para reter a manipulação de HTML.

Como preparar o CSS condicional

Usei o PostCSS para ativar a sintaxe das Consultas de mídia de nível 5, em que é possível armazenar um booleano de consulta de mídia em uma variável:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

Como preparar a condição do JS

No JavaScript, o navegador oferece uma maneira de verificar consultas de mídia. Usei a desestruturação para extrair e renomear o resultado booleano da verificação de consulta de mídia:

const {matches:motionOK} = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

Posso testar motionOK e mudar o documento apenas se o usuário não tiver solicitado a redução de movimento.

if (motionOK) {
  // document split manipulations
}

Posso verificar o mesmo valor usando o PostCSS para ativar a sintaxe @nest do esboço de aninhamento 1. Isso me permite armazenar toda a lógica sobre a animação e os requisitos de estilo dela para o elemento pai e os filhos em um só lugar:

letter-animation {
  @media (--motionOK) {
    /* animation styles */
  }
}

Com a propriedade personalizada do PostCSS e um booleano JavaScript, estamos prontos para atualizar o efeito condicionalmente. Isso nos leva à próxima seção, em que vou decompor o JavaScript para transformar strings em elementos.

Como dividir o texto

Letras, palavras, linhas etc. não podem ser animadas individualmente com CSS ou JS. Para conseguir o efeito, precisamos de caixas. Se quisermos animar cada letra, cada letra precisa ser um elemento. Se quisermos animar cada palavra, cada palavra precisa ser um elemento.

  1. Criar funções utilitárias JavaScript para dividir strings em elementos
  2. Orquestrar o uso desses utilitários

Função utilitária de divisão de letras

Um lugar divertido para começar é com uma função que recebe uma string e retorna cada letra em uma matriz.

export const byLetter = text =>
  [...text].map(span)

A sintaxe spread do ES6 ajudou muito a tornar essa tarefa rápida.

Função utilitária de divisão de palavras

Semelhante à divisão de letras, essa função recebe uma string e retorna cada palavra em uma matriz.

export const byWord = text =>
  text.split(' ').map(span)

O método split() em strings JavaScript permite especificar quais caracteres devem ser cortados. Eu passei por um espaço vazio, indicando uma divisão entre as palavras.

Como fazer a função utilitária de caixas

O efeito exige caixas para cada letra, e vemos nessas funções que map() está sendo chamado com uma função span(). Esta é a função span().

const span = (text, index) => {
  const node = document.createElement('span')

  node.textContent = text
  node.style.setProperty('--index', index)

  return node
}

É importante observar que uma propriedade personalizada chamada --index está sendo definida com a posição da matriz. Ter as caixas para as animações de letra é ótimo, mas ter um índice para usar no CSS é uma adição aparentemente pequena com um grande impacto. O mais notável nesse grande impacto é o estaggeramento. Vamos usar --index como uma forma de compensar animações para um visual escalado.

Conclusão dos utilitários

O módulo splitting.js foi concluído:

const span = (text, index) => {
  const node = document.createElement('span')

  node.textContent = text
  node.style.setProperty('--index', index)

  return node
}

export const byLetter = text =>
  [...text].map(span)

export const byWord = text =>
  text.split(' ').map(span)

Em seguida, importe e use essas funções byLetter() e byWord().

Orquestração dividida

Com os utilitários de divisão prontos para uso, reunir tudo significa:

  1. Como encontrar os elementos a serem divididos
  2. Dividir e substituir texto por HTML

Depois disso, o CSS assume o controle e anima os elementos / caixas.

Como encontrar elementos

Escolhi usar atributos e valores para armazenar informações sobre a animação desejado e como dividir o texto. Gostei de colocar essas opções declarativas no HTML. O atributo split-by é usado no JavaScript para encontrar elementos e criar caixas para letras ou palavras. O atributo letter-animation ou word-animation é usado no CSS para segmentar elementos filhos e aplicar transformações e animações.

Confira um exemplo de HTML que demonstra os dois atributos:

<h1 split-by="letter" letter-animation="breath">animated letters</h1>
<h1 split-by="word" word-animation="trampoline">hover the words</h1>

Como encontrar elementos no JavaScript

Usei a sintaxe do seletor CSS para presença de atributo para reunir a lista de elementos que querem dividir o texto:

const splitTargets = document.querySelectorAll('[split-by]')

Como encontrar elementos do CSS

Também usei o seletor de presença de atributo no CSS para dar a todas as animações de letra os mesmos estilos básicos. Mais tarde, vamos usar o valor do atributo para adicionar estilos mais específicos para conseguir um efeito.

letter-animation {
  @media (--motionOK) {
    /* animation styles */
  }
}

Dividir texto no lugar

Para cada um dos destinos divididos encontrados no JavaScript, vamos dividir o texto com base no valor do atributo e mapear cada string para um <span>. Podemos substituir o texto do elemento pelas caixas que criamos:

splitTargets.forEach(node => {
  const type = node.getAttribute('split-by')
  let nodes = null

  if (type === 'letter') {
    nodes = byLetter(node.innerText)
  }
  else if (type === 'word') {
    nodes = byWord(node.innerText)
  }

  if (nodes) {
    node.firstChild.replaceWith(...nodes)
  }
})

Conclusão da orquestração

index.js em conclusão:

import {byLetter, byWord} from './splitting.js'

const {matches:motionOK} = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

if (motionOK) {
  const splitTargets = document.querySelectorAll('[split-by]')

  splitTargets.forEach(node => {
    const type = node.getAttribute('split-by')
    let nodes = null

    if (type === 'letter')
      nodes = byLetter(node.innerText)
    else if (type === 'word')
      nodes = byWord(node.innerText)

    if (nodes)
      node.firstChild.replaceWith(...nodes)
  })
}

O JavaScript pode ser lido no seguinte inglês:

  1. Importe algumas funções auxiliares.
  2. Verifica se o movimento está ok para esse usuário. Se não, não faz nada.
  3. Para cada elemento que você quer dividir.
    1. Divida-os de acordo com a preferência deles.
    2. Substituir texto por elementos.

Como dividir animações e transições

A manipulação de documentos de divisão acima acabou de desbloquear uma infinidade de animações e efeitos em potencial com CSS ou JavaScript. Há alguns links na parte de baixo deste artigo para ajudar a dividir seu potencial.

É hora de mostrar o que você sabe! Vou compartilhar quatro animações e transições baseadas em CSS. 🤓

Letras separadas

Como base para os efeitos de letras divididas, achei o CSS a seguir útil. Coloquei todas as transições e animações atrás da consulta de mídia de movimento e depois dei a cada nova letra filha span uma propriedade de exibição e um estilo para saber o que fazer com espaços em branco:

[letter-animation] > span {
  display: inline-block;
  white-space: break-spaces;
}

O estilo de espaços em branco é importante para que os spans que são apenas um espaço não sejam colapsados pelo mecanismo de layout. Agora, vamos falar sobre as coisas divertidas com estado.

Exemplo de transição de letras divididas

Este exemplo usa transições CSS para o efeito de texto dividido. Com as transições, precisamos de estados para que o mecanismo possa animar entre eles. Escolhi três estados: sem cursor, cursor na frase e cursor em uma letra.

Quando o usuário passa o cursor sobre a frase, ou seja, o contêiner, eu reduzo o tamanho de todos os elementos filhos, como se o usuário os tivesse afastado. Em seguida, quando o usuário passa o cursor sobre uma letra, eu a trago para frente.

@media (--motionOK) {
  [letter-animation="hover"] {
    &:hover > span {
      transform: scale(.75);
    }

    & > span {
      transition: transform .3s ease;
      cursor: pointer;

      &:hover {
        transform: scale(1.25);
      }
    }
  }
}

Exemplo de animação de letras divididas

Este exemplo usa uma animação @keyframe predefinida para animar infinitamente cada letra e aproveita o índice de propriedade personalizada inline para criar um efeito de escalonamento.

@media (--motionOK) {
  [letter-animation="breath"] > span {
    animation:
      breath 1200ms ease
      calc(var(--index) * 100 * 1ms)
      infinite alternate;
  }
}

@keyframes breath {
  from {
    animation-timing-function: ease-out;
  }
  to {
    transform: translateY(-5px) scale(1.25);
    text-shadow: 0 0 25px var(--glow-color);
    animation-timing-function: ease-in-out;
  }
}

Dividir palavras

O Flexbox funcionou como um tipo de contêiner para mim nesses exemplos, usando bem a unidade ch como uma boa lacuna.

word-animation {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 1ch;
}
DevTools do Flexbox mostrando a lacuna entre as palavras

Exemplo de transição de palavras divididas

Neste exemplo de transição, uso o cursor novamente. Como o efeito inicialmente oculta o conteúdo até o passar do cursor, garanti que a interação e os estilos fossem aplicados apenas se o dispositivo tivesse a capacidade de passar o cursor.

@media (hover) {
  [word-animation="hover"] {
    overflow: hidden;
    overflow: clip;

    & > span {
      transition: transform .3s ease;
      cursor: pointer;

      &:not(:hover) {
        transform: translateY(50%);
      }
    }
  }
}

Exemplo de animação de palavras divididas

Neste exemplo de animação, uso o CSS @keyframes novamente para criar uma animação infinita escalonada em um parágrafo normal de texto.

[word-animation="trampoline"] > span {
  display: inline-block;
  transform: translateY(100%);
  animation:
    trampoline 3s ease
    calc(var(--index) * 150 * 1ms)
    infinite alternate;
}

@keyframes trampoline {
  0% {
    transform: translateY(100%);
    animation-timing-function: ease-out;
  }
  50% {
    transform: translateY(0);
    animation-timing-function: ease-in;
  }
}

Conclusão

Agora que você sabe como eu fiz, como você faria? 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie um Codepen ou hospede sua própria demonstração, envie um tweet para mim e vou adicionar à seção "Remixes da comunidade" abaixo.

Origem

Mais demonstrações e inspiração

Remixes da comunidade