Como criar animações de texto dividido

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

Nesta postagem, quero compartilhar ideias sobre como resolver animações e interações de texto dividido para a Web que sejam mínimas, acessíveis e funcionem em vários navegadores. Teste a demonstração.

Demonstração

Se você prefere vídeos, confira a versão deste post no YouTube:

Visão geral

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

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 divisão de texto em JavaScript.
  3. Orquestre as condicionais e os utilitários no carregamento da página.
  4. Escrever transições e animações CSS para letras e palavras (a parte legal!).

Confira uma prévia dos resultados condicionais que queremos:

captura de tela do Chrome DevTools com o painel "Elementos" aberto, o movimento reduzido definido como "reduzir" e o h1 mostrado sem divisão
O usuário prefere movimento reduzido: o texto é legível / não dividido

Se um usuário preferir movimento reduzido, deixamos o documento HTML como está e não fazemos animações. Se o movimento estiver bom, vamos dividir em partes. Confira uma prévia do HTML depois que o JavaScript dividiu o texto por letra.

captura de tela do Chrome DevTools com o painel "Elementos" aberto, o movimento reduzido definido como "reduzir" e o h1 mostrado sem divisão
O usuário não se importa com o movimento; o texto é dividido em vários elementos <span>

Preparando condicionais de movimento

A consulta de mídia @media (prefers-reduced-motion: reduce), que está convenientemente disponível, será usada no CSS e no JavaScript neste projeto. Essa consulta de mídia é nossa principal condicional 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 a condicional de CSS

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

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

Preparar a condicional 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 da consulta de mídia:

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

Em seguida, posso testar motionOK e só mudar o documento 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 Nesting Draft 1. Isso permite que eu armazene toda a lógica sobre a animação e os requisitos de estilo 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 do JavaScript, estamos prontos para fazer upgrade do efeito de forma condicional. Isso nos leva à próxima seção, em que analiso o JavaScript para transformar strings em elementos.

Como dividir texto

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

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

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

Uma forma divertida de 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 de espalhamento do ES6 realmente ajudou a tornar essa tarefa rápida.

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

Semelhante à divisão de letras, essa função usa 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 serão fatiados. Passei um espaço vazio, indicando uma divisão entre as palavras.

Função utilitária para criar caixas

O efeito requer caixas para cada letra, e vemos nessas funções que map() está sendo chamado com uma função span(). Veja 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 letras é ó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 escalonamento. Vamos usar --index como uma forma de compensar as animações para criar um visual escalonado.

Conclusão sobre utilitários

O módulo splitting.js em conclusão:

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 as funções byLetter() e byWord().

Orquestração de divisão

Com os utilitários de divisão prontos para uso, para juntar tudo, você precisa:

  1. Encontrar quais elementos dividir
  2. Dividir e substituir o 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 desejada 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.

Veja 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 do JavaScript

Usei a sintaxe do seletor de CSS para presença de atributo para coletar a lista de elementos que querem que o texto seja dividido:

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

Como encontrar elementos do CSS

Também usei o seletor de presença de atributo em CSS para dar a todas as animações de letras os mesmos estilos de base. Mais tarde, vamos usar o valor do atributo para adicionar estilos mais específicos e alcançar um efeito.

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

Dividir texto no local

Para cada um dos destinos de divisão encontrados no JavaScript, vamos dividir o texto com base no valor do atributo e mapear cada string para um <span>. Em seguida, 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 utilitárias auxiliares.
  2. Verifique se o movimento está correto para esse usuário. Se não estiver, não faça nada.
  3. Para cada elemento que precisa ser dividido.
    1. Divida-os com base em como eles querem ser divididos.
    2. Substitua o 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 inspirar seu potencial de divisão.

Hora de mostrar o que você pode fazer com isso! Vou compartilhar quatro animações e transições orientadas por CSS. 🤓

Dividir letras

Como base para os efeitos de letras divididas, achei o CSS a seguir útil. Coloquei todas as transições e animações por trás da consulta de mídia de movimento e, em seguida, dei a cada nova letra filha span uma propriedade de exibição e um estilo para 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 períodos que são apenas um espaço não sejam recolhidos pelo mecanismo de layout. Agora vamos para a parte divertida com estado.

Exemplo de letras divididas de transição

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

Quando o usuário passa o cursor sobre a frase, ou seja, o contêiner, eu reduzo todos os filhos como se o usuário os tivesse afastado. Depois, quando o usuário passa o mouse sobre uma letra, ela é trazida 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, aproveitando bem a unidade ch como um comprimento de lacuna saudável.

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

Exemplo de palavras de transição divididas

Neste exemplo de transição, uso o hover novamente. Como o efeito inicialmente oculta o conteúdo até que o usuário passe o cursor sobre ele, garanti que a interação e os estilos só fossem aplicados se o dispositivo tivesse essa capacidade.

@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 de texto normal.

[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 com ela e eu a adicionarei à seção de remixes da comunidade abaixo.

Origem

Mais demonstrações e inspiração

Remixes da comunidade