← Biblioteca componentes/conteudo/apresentacao-foto-alternada/ficha.md CódigoTela cheia
conteudo/apresentacao-foto-alternada

Apresentação com foto alternada

🟢 estavel complexidade baixa prefixo apres- quem-somos duas-colunas retrato slideshow crossfade titulo-gigante apresentacao sobre

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/conteudo/apresentacao-foto-alternada/estilo.css">
<script src="componentes/conteudo/apresentacao-foto-alternada/script.js"></script>

markup.html · 59 linhas · 2,8 KB

<!-- ==========================================================================
     APRESENTAÇÃO COM FOTO ALTERNADA — quem somos em duas colunas
     Prefixo: apres-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
     Sem lib. O revezamento das fotos é classe trocada por temporizador; a
     transição em si é CSS.

     A DOBRA. Coluna de retrato de um lado, título display gigante e dois
     parágrafos do outro. O título é a peça: ele ocupa metade da coluna de
     texto e é o que sustenta a dobra — por isso quebra em duas linhas na mão,
     com <br>, e não por largura de caixa. Quebra automática deixaria "SOMOS?"
     subir para a primeira linha em telas largas e a dobra perderia o degrau.

     O REVEZAMENTO. Três fotos empilhadas no mesmo quadro; só a que tem .is-on
     aparece. O script troca a cada --apres-hold. Duas fotos já funcionam,
     quatro é o teto útil: acima disso ninguém repara que voltou ao começo.

     ALT. Só a primeira foto descreve — as outras são o mesmo assunto em outro
     instante e repetir a descrição faria o leitor de tela anunciar três vezes
     a mesma coisa. Se as fotos mostrarem pessoas ou cenas DIFERENTES entre si,
     cada uma precisa do seu alt e o revezamento vira decisão editorial ruim:
     use o `midia/carrossel-perfis` ou uma galeria.

     ENTRADA. data-rv/data-d e .ttl do core, nunca observer próprio.
     ========================================================================== -->

<section class="sec-paper sec-pad apres" data-apres>
  <div class="wrap apres-grid">

    <figure class="apres-media" data-rv>
      <img class="apres-shot is-on" src="../../../showroom/demo/ph-1.webp"
           alt="Casal em uma varanda com vista para o vilarejo" decoding="async">
      <img class="apres-shot" src="../../../showroom/demo/ph-3.webp"
           alt="" aria-hidden="true" loading="lazy" decoding="async">
      <img class="apres-shot" src="../../../showroom/demo/ph-5.webp"
           alt="" aria-hidden="true" loading="lazy" decoding="async">
      <span class="grain"></span>
    </figure>

    <div class="apres-col">
      <h2 class="display-xl ttl">Quem<br>somos?</h2>

      <div class="apres-body">
        <p class="apres-p" data-rv>
          A Liberta é uma consultoria de investimentos independente, registrada
          como consultoria de valores mobiliários, que cuida do patrimônio de
          famílias e empresários no Brasil e fora dele.
        </p>
        <p class="apres-p" data-rv data-d="1">
          É conduzida por sócios vindos de mesas de private e de casas de
          gestão. A remuneração é uma taxa previamente acordada com o cliente,
          nunca comissão de produto, e a carteira fica na custódia da
          instituição que a família escolher.
        </p>
      </div>
    </div>

  </div>
</section>

estilo.css · 206 linhas · 8,5 KB

/* ==========================================================================
   APRESENTAÇÃO COM FOTO ALTERNADA — prefixo apres-
   Duas colunas: retrato que se reveza de um lado, título display gigante e
   dois parágrafos do outro.

   Tokens usados:
     --display --sans      tipografia do projeto
     --ease --dur          curva e duração de referência
     --fg --fg-2           título e corpo, herdados do escopo de tom da seção
     --radius-media        canto do retrato
     --sec-gap             respiro vertical (via .sec-pad do core)

   O componente NÃO declara escopo de tom. A seção declara .sec-paper (como no
   markup) ou .sec-dark, e título e texto seguem. É o que permite a mesma dobra
   abrir uma página clara e fechar uma escura sem tocar no CSS.
   ========================================================================== */

.apres{
  /* Corpo do título. O original de referência roda 160px numa display
     CONDENSADA — com ela, "SOMOS?" cabe em meia coluna. Numa display de
     largura normal o mesmo corpo estoura a coluna, então o teto aqui é mais
     baixo. Projeto com fonte condensada sobe este valor no tema e o resto
     do bloco acompanha sozinho.

     Quem desenha o título é a primitiva .display-xl do core; aqui só se
     escolhe o corpo. --apres-title continua sendo o gancho público desta
     dobra, e alimenta o parafuso da primitiva. */
  --apres-title:clamp(46px,6vw,104px);
  --display-xl:var(--apres-title);

  /* Tempo que cada foto fica na tela antes da troca. Lido pelo script.
     Abaixo de 2s a dobra pisca; acima de 5s ninguém vê a segunda foto. */
  --apres-hold:3.4s;

  /* Duração da fusão entre uma foto e a seguinte. */
  --apres-fade:1.1s;
}

/* Colunas iguais: a foto não é apoio do texto, é a outra metade da dobra.
   minmax(0,1fr) e não 1fr — sem o minmax, uma palavra longa no parágrafo
   empurra a coluna e desalinha o par. */
.apres-grid{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(32px,6vw,80px);
  align-items:center;
}

/* ---------------------------------------------------------------- retrato */
/* margin:0 porque é um <figure> e o navegador dá 1em de cada lado — o que
   quebraria o alinhamento da coluna sem que nada no CSS explicasse por quê. */
.apres-media{
  position:relative;
  margin:0;
  aspect-ratio:5/7;
  overflow:hidden;
  border-radius:var(--radius-media);
  /* As fotos são absolutas: sem isto, o quadro nasceria com altura zero
     enquanto a primeira imagem não decodifica. */
  background:var(--rule);
}

/* As três ocupam o mesmo quadro. Só a .is-on aparece.
   A que entra carrega blur indo a zero e um resto de escala — contrato §5,
   nada entra seco. A que sai faz o caminho inverso, então em nenhum quadro
   existem duas fotos nítidas ao mesmo tempo: o que se vê é uma virando a
   outra, não um empilhamento. */
.apres-shot{
  position:absolute;inset:0;
  width:100%;height:100%;object-fit:cover;
  opacity:0;filter:blur(12px);transform:scale(1.045);
  transition:opacity var(--apres-fade) var(--ease),
             filter var(--apres-fade) var(--ease),
             transform calc(var(--apres-fade) * 1.6) var(--ease);
}
.apres-shot.is-on{opacity:1;filter:blur(0);transform:scale(1)}

/* ---------------------------------------------------------------- texto */
.apres-col{
  display:flex;flex-direction:column;justify-content:center;
  gap:clamp(22px,3vw,32px);
  min-width:0;
}

/* O título é a primitiva .display-xl do core (ver core/base.css): as seis
   linhas que moravam aqui eram as mesmas do midia/carrossel-perfis e do
   conteudo/manifesto-com-imagens, e na terceira cópia o §6 manda promover.
   Sobrou o que é só desta dobra: nada. */

.apres-body{display:flex;flex-direction:column;gap:20px}

/* 60ch é a mesma medida do .sec-lead do core, e é o teto de linha confortável.
   Na coluna padrão ela quase encosta na borda — o parágrafo enche a coluna,
   como no original. Em tema de coluna larga ela segura a linha antes de a
   leitura ficar cansativa, que é o motivo de existir. */
.apres-p{
  font-size:16px;line-height:1.5;
  color:var(--fg-2);
  max-width:60ch;
}

/* Inverter os lados: retrato à direita, texto à esquerda. Mesma convenção do
   conteudo/imagem-lista-numerada, para as duas dobras virarem pelo mesmo
   gancho quando aparecem na mesma página. */
.apres.is-flipped .apres-media{order:2}

/* ---------------------------------------------------------------- movimento reduzido */
/* Quem pediu menos movimento fica com a primeira foto, parada — o script nem
   liga o temporizador (ver script.js). A regra abaixo cobre o caso do CSS
   colado sem o script: sem transição, a foto de cima simplesmente é a que se
   vê, e a dobra continua inteira. */
@media(prefers-reduced-motion:reduce){
  .apres-shot{transition:none}
}

/* ---------------------------------------------------------------- mobile */
/* ---- Variação: em colunas com rótulo (.apres-colunas) ----------------
   A mesma dobra de apresentação lida como página de revista: a frase em
   cima, na largura da coluna, e embaixo três colunas separadas por filete —
   o retrato vertical estreito e dois blocos de texto, cada um com o próprio
   rótulo entre parênteses. Os rótulos GRUDAM no alto da tela enquanto a
   coluna deles passa, e é isso que deixa ler "(Abordagem)" ao lado do
   parágrafo mesmo quando o parágrafo começa mais acima.

   Referência: a dobra "Sobre" da Skave (skave.co). Mesmo motor de fotos —
   o script não sabe que a variação existe; ele só alterna os .apres-shot da
   raiz. Seletores com a classe da variação na frente: vencem as regras da
   dobra-padrão em qualquer ordem (§6.4), e as media queries lá embaixo
   continuam valendo para a dobra-padrão sem tocar nesta. */
.apres-colunas{
  /* A frase aqui é enunciado, não título de duas palavras: corpo menor e
     entrelinha de frase longa — os dois parafusos do .display-xl do core. */
  --apres-title:clamp(30px,3.6vw,54px);
  --display-xl-lh:1.1;
}
.apres-colunas .apres-frase{
  max-width:26ch;
  margin-block:14px clamp(40px,7vh,72px);
}
.apres-colunas .apres-grid{
  /* O retrato é a coluna estreita, os dois blocos de texto são as largas —
     o contrário da dobra-padrão, em que a foto é metade da dobra. */
  grid-template-columns:minmax(160px,.72fr) minmax(0,1.3fr) minmax(0,1fr);
  gap:0;
  /* stretch, e não center: cada bloco precisa ter a altura da linha para
     o rótulo grudado ter por onde andar. */
  align-items:stretch;
}
.apres-colunas .apres-media{aspect-ratio:9/16}
.apres-bloco{
  border-inline-start:1px solid var(--rule);
  padding-inline:clamp(18px,2.4vw,34px);
}
/* O sticky é do conjunto rótulo + parágrafo, e funciona dentro do data-rv
   do bloco: filtro não interfere em sticky (MANUAL §5). Com o parágrafo
   mais curto que a coluna, o conjunto anda até o pé dela e para. */
.apres-fixo{
  position:sticky;
  inset-block-start:calc(var(--nav-h) + 24px);
  display:grid;
  gap:14px;
}
.apres-rot{
  font-family:var(--sans);
  font-weight:400;
  font-size:11px;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--fg-muted);
}

@media(max-width:900px){
  .apres-grid{grid-template-columns:1fr;gap:clamp(28px,5vh,44px)}
  /* Empilhado, o retrato de 5/7 come a tela inteira antes do texto começar.
     Achatar para 4/5 mantém o enquadramento sem virar uma parede. */
  .apres-media{aspect-ratio:4/5}
  /* A ordem invertida é decisão de desktop: empilhado, a foto vem sempre
     primeiro — é ela que abre a dobra. */
  .apres.is-flipped .apres-media{order:0}
}

@media(max-width:560px){
  .apres{--apres-title:clamp(38px,12vw,58px)}
  .apres-body{gap:16px}
}

/* A variação em colunas tem degraus próprios, e eles vêm por último para
   ficarem juntos. A 900px o retrato ocupa a coluna da esquerda nas duas
   linhas e os blocos empilham à direita; a 560px tudo vira uma coluna e o
   filete passa de lateral a superior. O rótulo deixa de grudar: empilhado,
   cada bloco tem a altura do próprio texto e não há o que acompanhar. */
@media(max-width:900px){
  .apres-colunas .apres-grid{grid-template-columns:minmax(120px,.8fr) minmax(0,1.2fr);row-gap:28px}
  .apres-colunas .apres-media{grid-row:span 2;aspect-ratio:9/16}
  .apres-colunas .apres-fixo{position:static}
}
@media(max-width:560px){
  .apres-colunas .apres-grid{grid-template-columns:minmax(0,1fr);row-gap:0}
  .apres-colunas .apres-media{grid-row:auto;aspect-ratio:4/5}
  .apres-bloco{
    border-inline-start:0;
    border-block-start:1px solid var(--rule);
    padding:20px 0 0;
    margin-block-start:28px;
  }
}

script.js · 78 linhas · 3,0 KB

/* ==========================================================================
   APRESENTAÇÃO COM FOTO ALTERNADA — o revezamento das fotos.

   A fusão em si é CSS (opacidade + blur + escala em .apres-shot). Este script
   só decide QUANDO trocar: tira .is-on de uma foto e põe na seguinte, a cada
   --apres-hold.

   Faz três coisas e nada além:
     1. lê o intervalo do próprio CSS, para o tempo morar num lugar só;
     2. só conta o tempo enquanto a dobra está na tela — dobra fora de vista
        não gasta temporizador, e quem chega nela vê a primeira foto, não a
        quarta;
     3. com movimento reduzido, nem liga: fica a primeira foto, parada.

   Sem listener de scroll: quem observa a entrada é o IntersectionObserver.
   ========================================================================== */

(function () {
  var raizes = document.querySelectorAll('[data-apres]');
  if (!raizes.length) return;                       // guard

  /* A flag do core é a fonte. O fallback existe para o componente colado
     sozinho, sem motion.js — e repete a regra do ?motion=on. */
  function reduzido() {
    if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  function iniciar(raiz) {
    var fotos = [].slice.call(raiz.querySelectorAll('.apres-shot'));
    /* Uma foto só é um retrato comum, e é um uso legítimo do componente:
       sai sem erro e sem temporizador. */
    if (fotos.length < 2) return;

    /* Movimento reduzido: a primeira foto é a dobra inteira. Sai antes de
       criar observador ou intervalo — não há o que desligar depois. */
    if (reduzido()) return;

    /* O intervalo mora no CSS (--apres-hold), junto do resto dos ajustes do
       componente. getPropertyValue devolve " 3.4s"; parseFloat resolve os
       dois problemas de uma vez, o espaço e a unidade. Um valor em ms no
       tema quebraria a conta — por isso o CSS documenta que é em segundos. */
    var espera = parseFloat(getComputedStyle(raiz).getPropertyValue('--apres-hold')) || 3.4;

    var atual = 0;
    var relogio = 0;

    function trocar() {
      fotos[atual].classList.remove('is-on');
      atual = (atual + 1) % fotos.length;
      fotos[atual].classList.add('is-on');
    }

    function ligar() {
      if (relogio) return;
      relogio = setInterval(trocar, espera * 1000);
    }

    function desligar() {
      if (!relogio) return;
      clearInterval(relogio);
      relogio = 0;
    }

    /* threshold baixo de propósito: o revezamento tem de já estar rodando
       quando a dobra termina de subir, não começar depois que ela parou. */
    var io = new IntersectionObserver(function (entradas) {
      for (var i = 0; i < entradas.length; i++) {
        if (entradas[i].isIntersecting) ligar(); else desligar();
      }
    }, { threshold: .12 });

    io.observe(raiz);
  }

  for (var r = 0; r < raizes.length; r++) iniciar(raizes[r]);
})();

Duas colunas de peso igual. De um lado um retrato alto que não fica parado: três fotos ocupam o mesmo quadro e se revezam em fusão com blur, uma virando a outra a cada três segundos e meio. Do outro, um título display em caixa alta, grande o bastante para ocupar metade da coluna, quebrado na mão em duas linhas, e embaixo dois parágrafos curtos.

Resolve a dobra "quem somos" sem cair no par foto-estática-mais-texto que toda página institucional tem. O revezamento faz o trabalho que uma foto só não faz: mostra que existe mais de um momento, mais de uma pessoa, mais de um dia — sem pedir um carrossel, sem controle nenhum para operar e sem tirar o olho do texto, que é o que a pessoa veio ler.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Título Duas linhas, uma palavra por linha sempre que der. Caixa alta é do CSS — escreva normal no markup. A quebra é <br> na mão, nunca por largura.
Parágrafo 1 O que a empresa é. Uma frase de definição, sem adjetivo. Três a quatro linhas.
Parágrafo 2 De onde ela vem ou por quem é feita. É o parágrafo que dá lastro ao primeiro. Quatro a cinco linhas.
Fotos 2 a 4. A primeira é a que a pessoa vê ao chegar — é ela que precisa ser a melhor.

O revezamento só lê como intenção se as fotos forem parentes: mesma distância, mesma luz, mesmo corte. Três fotos com enquadramentos diferentes viram pisca-pisca. Se uma delas for claramente a melhor, ponha-a em primeiro e aceite duas — duas fotos bem casadas valem mais que quatro desiguais.

Especificação dos assets


Como usar

<link rel="stylesheet" href="componentes/conteudo/apresentacao-foto-alternada/estilo.css">
<script src="componentes/conteudo/apresentacao-foto-alternada/script.js"></script>

Ganchos

Gancho Papel
[data-apres] Raiz. O script parte daqui e nunca do documento — a dobra pode repetir na página.
--apres-title Corpo do título. clamp(46px,6vw,104px) no padrão. Alimenta o --display-xl da primitiva do core.
--apres-hold Quanto cada foto fica na tela, em segundos. 3.4s no padrão. O script lê daqui.
--apres-fade Duração da fusão entre uma foto e a próxima. 1.1s no padrão.
.is-flipped Na seção: inverte os lados no desktop.
.is-on Na foto: qual está visível. Quem escreve o markup marca só a primeira; o resto é do script.

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
referência: Rony Meisler https://www.ronymeisler.com.br/investimentos — dobra "Quem somos?" 2026-08-31 referência visual, não origem de código
referência: Skave https://www.skave.co/pt — dobra "Sobre" (rótulos "(Abordagem)", "(Proposta de valor)") 2026-10-01 variação em colunas com rótulo: o vídeo vertical da referência virou o mesmo revezamento de fotos da dobra-padrão, em 9:16; os rótulos grudados vieram de lá. Next.js e Tailwind não atravessaram

Origem. O componente foi escrito do zero a partir da observação da dobra "Quem somos?" da página de investimentos de ronymeisler.com.br — site publicado em Framer, cujo CSS é gerado por máquina, com classes hasheadas e sem licença declarada. Nenhuma linha de lá foi copiada: o que veio foi a medida (proporção das colunas, escala do título, ritmo do texto) lida no navegador, e a ideia. Por isso o status é externo: nunca rodou em produção nossa. O primeiro site que usar promove — e registra qual foi. A referência está registrada em projetos/referencias-externas.md.

Normalizações aplicadas na tradução:

Divergências conhecidas vs. a referência: